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,68 @@
// 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();
SecurityTokenInvalidAudienceException securityTokenInvalidAudienceException = _exception as SecurityTokenInvalidAudienceException;
if (securityTokenInvalidAudienceException != null)
iNinja marked this conversation as resolved.
Show resolved Hide resolved
{
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
137 changes: 135 additions & 2 deletions src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,30 @@

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using System.Threading;
using Microsoft.IdentityModel.Abstractions;
using Microsoft.IdentityModel.Logging;

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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How much work would it be to change IEnumberable to IList?

Copy link
Member

@brentschmaltz brentschmaltz Jun 20, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember this call will arrive from

TokenHandler.ValidateTokenAsync(
    string token,
    TokenValidationParameters validationParameters,
    CallContext callContext, 
    CancellationToken cancellationToken)
{
    // ...
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use IList on the new ValidateAudience method.

SecurityToken securityToken,
TokenValidationParameters validationParameters,
CallContext callContext);

/// <summary>
/// Partial class for Audience Validation.
/// </summary>
Expand Down Expand Up @@ -88,7 +106,121 @@ public static void ValidateAudience(IEnumerable<string> audiences, SecurityToken
throw LogHelper.LogExceptionMessage(ex);
}


#nullable enable
/// <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(IEnumerable<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 (securityToken == null)
return new AudienceValidationResult(
Utility.SerializeAsSingleCommaDelimitedString(audiences),
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10000,
LogHelper.MarkAsNonPII(nameof(securityToken))),
typeof(ArgumentNullException),
new StackFrame(true),
null));

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.Any())
return new AudienceValidationResult(
Utility.SerializeAsSingleCommaDelimitedString(audiences),
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10206,
null),
typeof(SecurityTokenInvalidAudienceException),
new StackFrame(true)));

// create enumeration of all valid audiences from validationParameters
IEnumerable<string> validationParametersAudiences;

if (validationParameters.ValidAudiences == null)
validationParametersAudiences = new[] { validationParameters.ValidAudience };
iNinja marked this conversation as resolved.
Show resolved Hide resolved
else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience))
validationParametersAudiences = validationParameters.ValidAudiences;
else
validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience });

string? validAudience = AudienceIsValidReturning(audiences, validationParameters, validationParametersAudiences);
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(IEnumerable<string> audiences, TokenValidationParameters validationParameters, IEnumerable<string> validationParametersAudiences)
{
return AudienceIsValidReturning(audiences, validationParameters, validationParametersAudiences) != null;
}

private static string? AudienceIsValidReturning(IEnumerable<string> audiences, TokenValidationParameters validationParameters, IEnumerable<string> validationParametersAudiences)
{
foreach (string tokenAudience in audiences)
{
Expand All @@ -105,13 +237,14 @@ private static bool AudienceIsValid(IEnumerable<string> audiences, TokenValidati
if (LogHelper.IsEnabled(EventLogLevel.Informational))
LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience));

return true;
return validAudience;
}
}
}

return false;
return null;
}
#nullable disable

private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience)
{
Expand Down
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
66 changes: 66 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,9 @@ public static bool AreIssuerValidationResultsEqual(object object1, object object
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}");

Expand Down Expand Up @@ -607,6 +610,69 @@ public static bool AreIssuerValidationResultsEqual(object object1, object object
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);
Expand Down
Loading