Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Audience validation: remove exceptions #2655

Merged
merged 12 commits into from
Jun 25, 2024
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -880,7 +880,7 @@ protected virtual void SetDelegateFromAttribute(SamlAttribute attribute, ClaimsI
/// <param name="audiences"><see cref="IEnumerable{String}"/>.</param>
/// <param name="securityToken">The <see cref="SamlSecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <remarks>see <see cref="Validators.ValidateAudience"/> for additional details.</remarks>
/// <remarks>see <see cref="Validators.ValidateAudience(IEnumerable{string}, SecurityToken, TokenValidationParameters)"/> for additional details.</remarks>
protected virtual void ValidateAudience(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
Validators.ValidateAudience(audiences, securityToken, validationParameters);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// Contains the result of validating the audiences from a <see cref="SecurityToken"/>.
/// The <see cref="TokenValidationResult"/> contains a collection of <see cref="ValidationResult"/> for each step in the token validation.
/// </summary>
internal class AudienceValidationResult : ValidationResult
{
private Exception _exception;

/// <summary>
/// Creates an instance of <see cref="AudienceValidationResult"/>.
/// </summary>
/// <paramref name="audience"/> is the audience that was validated successfully.
public AudienceValidationResult(string audience) : base(ValidationFailureType.ValidationSucceeded)
{
IsValid = true;
Audience = audience;
}

/// <summary>
/// Creates an instance of <see cref="IssuerValidationResult"/>
/// </summary>
/// <paramref name="audience"/> is the audience that was intended to be validated.
/// <paramref name="validationFailure"/> is the <see cref="ValidationFailureType"/> that occurred during validation.
/// <paramref name="exceptionDetail"/> is the <see cref="ExceptionDetail"/> that occurred during validation.
public AudienceValidationResult(string audience, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
: base(validationFailure, exceptionDetail)
{
IsValid = false;
Audience = audience;
}

/// <summary>
/// Gets the <see cref="Exception"/> that occurred during validation.
/// </summary>
public override Exception Exception
{
get
{
if (_exception != null || ExceptionDetail == null)
westin-m marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}

/// <summary>
/// Gets the audience that was validated or intended to be validated.
/// </summary>
public string Audience { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) { } }

/// <summary>
/// Defines a type that represents that audience validation failed.
/// </summary>
public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed");
private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } }

/// <summary>
/// Defines a type that represents that no evaluation has taken place.
/// </summary>
Expand Down
183 changes: 160 additions & 23 deletions src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
/// <summary>
/// Definition for delegate that will validate the audiences value in a token.
/// </summary>
/// <param name="audiences">The audiences to validate.</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> that is being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="callContext"></param>
/// <returns>A <see cref="IssuerValidationResult"/>that contains the results of validating the issuer.</returns>
/// <remarks>This delegate is not expected to throw.</remarks>
internal delegate AudienceValidationResult ValidateAudience(
IEnumerable<string> audiences,
iNinja marked this conversation as resolved.
Show resolved Hide resolved
SecurityToken? securityToken,
TokenValidationParameters validationParameters,
CallContext callContext);

/// <summary>
/// Partial class for Audience Validation.
/// </summary>
Expand Down Expand Up @@ -62,17 +79,10 @@ public static void ValidateAudience(IEnumerable<string> audiences, SecurityToken
new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206))
{ InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) });

// create enumeration of all valid audiences from validationParameters
IEnumerable<string> 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<string> audiencesAsList)
audiencesAsList = audiences.ToList();

if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences))
if (AudienceIsValid(audiencesAsList, validationParameters))
keegan-caruso marked this conversation as resolved.
Show resolved Hide resolved
return;

SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException(
Expand All @@ -88,39 +98,165 @@ public static void ValidateAudience(IEnumerable<string> audiences, SecurityToken
throw LogHelper.LogExceptionMessage(ex);
}

private static bool AudienceIsValid(IEnumerable<string> audiences, TokenValidationParameters validationParameters, IEnumerable<string> validationParametersAudiences)

/// <summary>
/// Determines if the audiences found in a <see cref="SecurityToken"/> are valid.
/// </summary>
/// <param name="audiences">The audiences found in the <see cref="SecurityToken"/>.</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="callContext"></param>
/// <exception cref="ArgumentNullException">If 'validationParameters' is null.</exception>
/// <exception cref="ArgumentNullException">If 'audiences' is null and <see cref="TokenValidationParameters.ValidateAudience"/> is true.</exception>
/// <exception cref="SecurityTokenInvalidAudienceException">If <see cref="TokenValidationParameters.ValidAudience"/> is null or whitespace and <see cref="TokenValidationParameters.ValidAudiences"/> is null.</exception>
/// <exception cref="SecurityTokenInvalidAudienceException">If none of the 'audiences' matched either <see cref="TokenValidationParameters.ValidAudience"/> or one of <see cref="TokenValidationParameters.ValidAudiences"/>.</exception>
/// <remarks>An EXACT match is required.</remarks>
#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
internal static AudienceValidationResult ValidateAudience(IList<string> audiences, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext)
#pragma warning restore CA1801
{
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.Count == 0)
return new AudienceValidationResult(
Utility.SerializeAsSingleCommaDelimitedString(audiences),
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10206,
null),
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true)));

string? validAudience = AudienceIsValidReturning(audiences, validationParameters);
if (validAudience != null)
{
return new AudienceValidationResult(validAudience);
}

return new AudienceValidationResult(
Utility.SerializeAsSingleCommaDelimitedString(audiences),
ValidationFailureType.AudienceValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10214,
LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiences)),
LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"),
LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences))),
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true)));
}

private static bool AudienceIsValid(IList<string> audiences, TokenValidationParameters validationParameters)
{
return AudienceIsValidReturning(audiences, validationParameters) != null;
}

private static string? AudienceIsValidReturning(IList<string> 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<string> validAudiences)
validAudiences = validationParameters.ValidAudiences.ToList();

validAudience = AudiencesMatchList(audiences, validAudiences, validationParameters.IgnoreTrailingSlashWhenValidatingAudience);
}

return validAudience;
}

private static string? AudiencesMatchSingle(IList<string> audiences, string validAudience, bool ignoreTrailingSlashWhenValidatingAudience)
{
foreach (string tokenAudience in audiences)
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<string> audiences, List<string> 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;
Expand Down Expand Up @@ -152,3 +288,4 @@ private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, st

}
}
#nullable disable
Original file line number Diff line number Diff line change
Expand Up @@ -1639,7 +1639,7 @@ protected virtual string CreateActorValue(ClaimsIdentity actor)
/// <param name="audiences">The audiences found in the <see cref="JwtSecurityToken"/>.</param>
/// <param name="jwtToken">The <see cref="JwtSecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <remarks>See <see cref="Validators.ValidateAudience"/> for additional details.</remarks>
/// <remarks>See <see cref="Validators.ValidateAudience(IEnumerable{string}, SecurityToken, TokenValidationParameters)"/> for additional details.</remarks>
protected virtual void ValidateAudience(IEnumerable<string> audiences, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
{
Validators.ValidateAudience(audiences, jwtToken, validationParameters);
Expand Down
Loading