diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs index 9b70430151..a75c33da3a 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.cs @@ -880,7 +880,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI /// . /// The being validated. /// required for validation. - /// see for additional details. + /// see for additional details. protected virtual void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) { Validators.ValidateAudience(audiences, securityToken, validationParameters); diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs new file mode 100644 index 0000000000..024cca68ba --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating the audiences from a . + /// The contains a collection of for each step in the token validation. + /// + internal class AudienceValidationResult : ValidationResult + { + private Exception _exception; + + /// + /// Creates an instance of . + /// + /// is the audience that was validated successfully. + public AudienceValidationResult(string audience) : base(ValidationFailureType.ValidationSucceeded) + { + IsValid = true; + Audience = audience; + } + + /// + /// Creates an instance of + /// + /// is the audience that was intended to be validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public AudienceValidationResult(string audience, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + IsValid = false; + Audience = audience; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + if (_exception is SecurityTokenInvalidAudienceException securityTokenInvalidAudienceException) + { + securityTokenInvalidAudienceException.InvalidAudience = Audience; + securityTokenInvalidAudienceException.ExceptionDetail = ExceptionDetail; + securityTokenInvalidAudienceException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the audience that was validated or intended to be validated. + /// + public string Audience { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 3d872bb63a..4c561994fb 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -33,6 +33,12 @@ private class NullArgumentFailure : ValidationFailureType { internal NullArgumen public static readonly ValidationFailureType IssuerValidationFailed = new IssuerValidationFailure("IssuerValidationFailed"); private class IssuerValidationFailure : ValidationFailureType { internal IssuerValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that audience validation failed. + /// + public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed"); + private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that no evaluation has taken place. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs index 9c41082c53..e56c166e70 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs @@ -3,12 +3,29 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; +#nullable enable namespace Microsoft.IdentityModel.Tokens { + /// + /// Definition for delegate that will validate the audiences value in a token. + /// + /// The audiences to validate. + /// The that is being validated. + /// required for validation. + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate AudienceValidationResult ValidateAudience( + IEnumerable audiences, + SecurityToken? securityToken, + TokenValidationParameters validationParameters, + CallContext callContext); + /// /// Partial class for Audience Validation. /// @@ -62,17 +79,10 @@ public static void ValidateAudience(IEnumerable audiences, SecurityToken new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - // create enumeration of all valid audiences from validationParameters - IEnumerable validationParametersAudiences; - - if (validationParameters.ValidAudiences == null) - validationParametersAudiences = new[] { validationParameters.ValidAudience }; - else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) - validationParametersAudiences = validationParameters.ValidAudiences; - else - validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); + if (audiences is not List audiencesAsList) + audiencesAsList = audiences.ToList(); - if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) + if (AudienceIsValid(audiencesAsList, validationParameters)) return; SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( @@ -88,39 +98,168 @@ public static void ValidateAudience(IEnumerable audiences, SecurityToken throw LogHelper.LogExceptionMessage(ex); } - private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) + + /// + /// Determines if the audiences found in a are valid. + /// + /// The audiences found in the . + /// The being validated. + /// required for validation. + /// + /// If 'validationParameters' is null. + /// If 'audiences' is null and is true. + /// If is null or whitespace and is null. + /// If none of the 'audiences' matched either or one of . + /// An EXACT match is required. +#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging + internal static AudienceValidationResult ValidateAudience(IEnumerable audiences, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 { - foreach (string tokenAudience in audiences) + if (validationParameters == null) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true))); + + if (!validationParameters.ValidateAudience) { + LogHelper.LogWarning(LogMessages.IDX10233); + return new AudienceValidationResult(Utility.SerializeAsSingleCommaDelimitedString(audiences)); + } + + if (audiences == null) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10207, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiences), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + if (audiences is not List audiencesAsList) + audiencesAsList = audiences.ToList(); + + if (audiencesAsList.Count == 0) + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList), + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10206, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + + string? validAudience = AudienceIsValidReturning(audiencesAsList, validationParameters); + if (validAudience != null) + { + return new AudienceValidationResult(validAudience); + } + + return new AudienceValidationResult( + Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList), + ValidationFailureType.AudienceValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiencesAsList)), + LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences))), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true))); + } + + private static bool AudienceIsValid(List audiences, TokenValidationParameters validationParameters) + { + return AudienceIsValidReturning(audiences, validationParameters) != null; + } + + private static string? AudienceIsValidReturning(List audiences, TokenValidationParameters validationParameters) + { + string? validAudience = null; + if (!string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) + validAudience = AudiencesMatchSingle(audiences, validationParameters.ValidAudience, validationParameters.IgnoreTrailingSlashWhenValidatingAudience); + + if (validAudience == null && validationParameters.ValidAudiences != null) + { + if (validationParameters.ValidAudiences is not List validAudiences) + validAudiences = validationParameters.ValidAudiences.ToList(); + + validAudience = AudiencesMatchList(audiences, validAudiences, validationParameters.IgnoreTrailingSlashWhenValidatingAudience); + } + + return validAudience; + } + + private static string? AudiencesMatchSingle(List audiences, string validAudience, bool ignoreTrailingSlashWhenValidatingAudience) + { + for (int i = 0; i < audiences.Count; i++) + { + string tokenAudience = audiences[i]; if (string.IsNullOrWhiteSpace(tokenAudience)) continue; - foreach (string validAudience in validationParametersAudiences) + if (AudiencesMatch(ignoreTrailingSlashWhenValidatingAudience, tokenAudience, validAudience)) { - if (string.IsNullOrWhiteSpace(validAudience)) + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return tokenAudience; + } + } + + return null; + } + + private static string? AudiencesMatchList(IList audiences, List validAudiences, bool ignoreTrailingSlashWhenValidatingAudience) + { + for (int i = 0; i < audiences.Count; i++) + { + string tokenAudience = audiences[i]; + if (string.IsNullOrWhiteSpace(tokenAudience)) + continue; + + foreach (string validAudience in validAudiences) + { + if (string.IsNullOrEmpty(validAudience)) continue; - if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) + if (AudiencesMatch(ignoreTrailingSlashWhenValidatingAudience, tokenAudience, validAudience)) { if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - return true; + return tokenAudience; } } } - return false; + return null; } - private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) + private static bool AudiencesMatch(bool ignoreTrailingSlashWhenValidatingAudience, string tokenAudience, string validAudience) { if (validAudience.Length == tokenAudience.Length) - { - if (string.Equals(validAudience, tokenAudience)) - return true; - } - else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) + return string.Equals(validAudience, tokenAudience); + else if (ignoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) return true; return false; @@ -152,3 +291,4 @@ private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, st } } +#nullable disable diff --git a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs index 4308d48d77..7fda27db7f 100644 --- a/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs +++ b/src/System.IdentityModel.Tokens.Jwt/JwtSecurityTokenHandler.cs @@ -1639,7 +1639,7 @@ protected virtual string CreateActorValue(ClaimsIdentity actor) /// The audiences found in the . /// The being validated. /// required for validation. - /// See for additional details. + /// See for additional details. protected virtual void ValidateAudience(IEnumerable audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters) { Validators.ValidateAudience(audiences, jwtToken, validationParameters); diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 7ef5e4fa3e..c8caa5156d 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -574,6 +574,9 @@ internal static bool AreIssuerValidationResultsEqual( if (issuerValidationResult1.Issuer != issuerValidationResult2.Issuer) localContext.Diffs.Add($"IssuerValidationResult1.Issuer: {issuerValidationResult1.Issuer} != IssuerValidationResult2.Issuer: {issuerValidationResult2.Issuer}"); + if (issuerValidationResult1.IsValid != issuerValidationResult2.IsValid) + localContext.Diffs.Add($"IssuerValidationResult1.IsValid: {issuerValidationResult1.IsValid} != IssuerValidationResult2.IsValid: {issuerValidationResult2.IsValid}"); + if (issuerValidationResult1.Source != issuerValidationResult2.Source) localContext.Diffs.Add($"IssuerValidationResult1.Source: {issuerValidationResult1.Source} != IssuerValidationResult2.Source: {issuerValidationResult2.Source}"); @@ -607,6 +610,69 @@ internal static bool AreIssuerValidationResultsEqual( return context.Merge(localContext); } + public static bool AreAudienceValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreAudienceValidationResultsEqual( + object1 as AudienceValidationResult, + object2 as AudienceValidationResult, + "AudienceValidationResult1", + "AudienceValidationResult2", + null, + context); + } + + internal static bool AreAudienceValidationResultsEqual( + AudienceValidationResult audienceValidationResult1, + AudienceValidationResult audienceValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(audienceValidationResult1, audienceValidationResult2, localContext)) + return context.Merge(localContext); + + if (audienceValidationResult1.Audience != audienceValidationResult2.Audience) + localContext.Diffs.Add($"AudienceValidationResult1.Audience: '{audienceValidationResult1.Audience}' != AudienceValidationResult2.Audience: '{audienceValidationResult2.Audience}'"); + + if (audienceValidationResult1.IsValid != audienceValidationResult2.IsValid) + localContext.Diffs.Add($"AudienceValidationResult1.IsValid: {audienceValidationResult1.IsValid} != AudienceValidationResult2.IsValid: {audienceValidationResult2.IsValid}"); + + // true => both are not null. + if (ContinueCheckingEquality(audienceValidationResult1.Exception, audienceValidationResult2.Exception, localContext)) + { + AreStringsEqual( + audienceValidationResult1.Exception.Message, + audienceValidationResult2.Exception.Message, + $"({name1})audienceValidationResult1.Exception.Message", + $"({name2})audienceValidationResult2.Exception.Message", + localContext); + + AreStringsEqual( + audienceValidationResult1.Exception.Source, + audienceValidationResult2.Exception.Source, + $"({name1})audienceValidationResult1.Exception.Source", + $"({name2})audienceValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + audienceValidationResult1.Exception.StackTrace.Trim(), + audienceValidationResult2.Exception.StackTrace.Trim(), + $"({name1})audienceValidationResult1.Exception.StackTrace", + $"({name2})audienceValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs new file mode 100644 index 0000000000..aa6a45b784 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AudienceValidationResultTests.cs @@ -0,0 +1,597 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Xunit; +using System.Collections.Generic; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class AudienceValidationResultTests + { + [Theory, MemberData(nameof(ValidateAudienceParametersTestCases), DisableDiscoveryEnumeration = true)] + public void ValidateAudienceParameters(AudienceValidationTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.AudienceValidatorResultTests", theoryData); + + AudienceValidationResult audienceValidationResult = Validators.ValidateAudience( + theoryData.Audiences, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (audienceValidationResult.Exception == null) + theoryData.ExpectedException.ProcessNoException(); + else + theoryData.ExpectedException.ProcessException(audienceValidationResult.Exception, context); + + IdentityComparer.AreAudienceValidationResultsEqual( + audienceValidationResult, + theoryData.AudienceValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateAudienceParametersTestCases + { + get + { + return new TheoryData + { + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + TestId = "ValidationParametersNull", + ValidationParameters = null, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("validationParameters")), + typeof(ArgumentNullException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + "", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(""), + LogHelper.MarkAsNonPII("audience"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { " " }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + " ", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(" "), + LogHelper.MarkAsNonPII("audience"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = null, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10207:"), + TestId = "AudiencesNull", + AudienceValidationResult = new AudienceValidationResult( + "null", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10207, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List{ }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10206:"), + TestId = "AudiencesEmptyList", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "audience"}, + AudienceValidationResult = new AudienceValidationResult( + "empty", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10206, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List{ }, + TestId = "ValidateAudienceFalseAudiencesEmptyList", + ValidationParameters = new TokenValidationParameters{ ValidateAudience = false }, + AudienceValidationResult = new AudienceValidationResult("empty") + }, + new AudienceValidationTheoryData + { + Audiences = null, + TestId = "ValidateAudienceFalseAudiencesNull", + ValidationParameters = new TokenValidationParameters{ ValidateAudience = false }, + AudienceValidationResult = new AudienceValidationResult("null") + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidAudienceEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudience = "" }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidAudienceWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudience = " " }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesEmptyString", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = new List{ "" } }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII("audience1"), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII("")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWhiteSpace", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = new List{ " " } }, + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII("audience1"), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(" ")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = new List { "audience1" }, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10208:"), + TestId = "ValidateAudienceTrueValidAudienceAndValidAudiencesNull", + AudienceValidationResult = new AudienceValidationResult( + "audience1", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10208, + null), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + } + }; + } + } + + [Theory, MemberData(nameof(ValidateAudienceTheoryData))] + public void ValidateAudience(AudienceValidationTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.ValidateAudience", theoryData); + AudienceValidationResult audienceValidationResult = Validators.ValidateAudience( + theoryData.Audiences, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (audienceValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(audienceValidationResult.Exception); + else + theoryData.ExpectedException.ProcessNoException(context); + + IdentityComparer.AreAudienceValidationResultsEqual( + audienceValidationResult, + theoryData.AudienceValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData ValidateAudienceTheoryData + { + get + { + var audience1 = "http://audience1.com"; + var audience2 = "http://audience2.com"; + List audiences1 = new List { "", audience1 }; + List audiences1WithSlash = new List { "", audience1 + "/" }; + List audiences1WithTwoSlashes = new List { "", audience1 + "//" }; + List audiences2 = new List { "", audience2 }; + List audiences2WithSlash = new List { "", audience2 + "/" }; + + var commaAudience1 = ", " + audience1; + var commaAudience2 = ", " + audience2; + var audience1Slash = audience1 + "/"; + var audience2Slash = audience2 + "/"; + var commaAudience1Slash = commaAudience1 + "/"; + var commaAudience2Slash = commaAudience2 + "/"; + + return new TheoryData + { + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "SameLengthMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "SameLengthNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2 }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience2), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "NoMatchTVPValidateFalse", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2, ValidateAudience = false }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult(commaAudience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesValidAudienceWithSlashNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience2 + "/" }, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, "Issuer"), + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience2Slash), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences2WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "AudiencesWithSlashValidAudienceSameLengthNotMatched", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience2Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience2Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 + "/" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1Slash), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "ValidAudienceWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "/" }, + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudiences = audiences1WithSlash }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience1Slash)), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + TestId = "ValidAudiencesWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences1WithSlash }, + AudienceValidationResult = new AudienceValidationResult(audience1) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithExtraChar", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "A" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1 + "A"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudienceWithDoubleSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 + "//" }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII(audience1 + "//"), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "ValidAudiencesWithDoubleSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences1WithTwoSlashes }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience1 + "//")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + TestId = "TokenAudienceWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult(audience1Slash) + }, + new AudienceValidationTheoryData + { + Audiences = audiences2WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithSlashNotEqual", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience2Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience2Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudiencesWithSlashTVPFalse", + ValidationParameters = new TokenValidationParameters{ IgnoreTrailingSlashWhenValidatingAudience = false, ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + TestId = "TokenAudiencesWithSlashTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult(audience1Slash) + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithSlash, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudiencesWithSlashValidAudiencesNotMatchedTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudiences = audiences2 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1Slash, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1Slash), + LogHelper.MarkAsNonPII("null"), + LogHelper.MarkAsNonPII(commaAudience2)), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + }, + new AudienceValidationTheoryData + { + Audiences = audiences1WithTwoSlashes, + ExpectedException = ExpectedException.SecurityTokenInvalidAudienceException("IDX10214:"), + TestId = "TokenAudienceWithTwoSlashesTVPTrue", + ValidationParameters = new TokenValidationParameters{ ValidAudience = audience1 }, + AudienceValidationResult = new AudienceValidationResult( + commaAudience1 + "//", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10214, + LogHelper.MarkAsNonPII(commaAudience1 + "//"), + LogHelper.MarkAsNonPII(audience1), + LogHelper.MarkAsNonPII("null")), + typeof(SecurityTokenInvalidAudienceException), + new StackFrame(true), + null)), + } + }; + } + } + + public class AudienceValidationTheoryData : TheoryDataBase + { + public List Audiences { get; set; } + + internal AudienceValidationResult AudienceValidationResult { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } = new TokenValidationParameters(); + + internal ValidationFailureType ValidationFailureType { get; set; } + } + + + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index c8b5694cc6..51ad101026 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -32,6 +32,8 @@ public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData th if (issuerValidationResult.Exception != null) theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + else + theoryData.ExpectedException.ProcessNoException(); IdentityComparer.AreIssuerValidationResultsEqual( issuerValidationResult,