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

Re-factor Issuer Validator to Follow New Validation Model #2759

Merged
merged 4 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Tokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 IDX10222 = "IDX10222: Lifetime validation failed. The token is not yet valid. ValidFrom (UTC): '{0}', Current time (UTC): '{1}'.";
public const string IDX10223 = "IDX10223: Lifetime validation failed. The token is expired. ValidTo (UTC): '{0}', Current time (UTC): '{1}'.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ internal class ValidationParameters
private string _nameClaimType = ClaimsIdentity.DefaultNameClaimType;
private string _roleClaimType = ClaimsIdentity.DefaultRoleClaimType;
private Dictionary<string, object> _instancePropertyBag;
private IList<string> _validIssuers;
private IList<string> _validTokenTypes = [];

private AlgorithmValidatorDelegate _algorithmValidator = Validators.ValidateAlgorithm;
Expand Down Expand Up @@ -90,7 +91,7 @@ protected ValidationParameters(ValidationParameters other)
ValidateWithLKG = other.ValidateWithLKG;
ValidAlgorithms = other.ValidAlgorithms;
ValidAudiences = other.ValidAudiences;
ValidIssuers = other.ValidIssuers;
_validIssuers = other.ValidIssuers;
ValidTypes = other.ValidTypes;
}

Expand Down Expand Up @@ -519,9 +520,13 @@ public TypeValidatorDelegate TypeValidator

/// <summary>
/// Gets the <see cref="IList{String}"/> that contains valid issuers that will be used to check against the token's issuer.
/// The default is <c>null</c>.
/// The default is an empty collection.
/// </summary>
public IList<string> ValidIssuers { get; }
/// <returns>The <see cref="IList{String}"/> that contains valid issuers that will be used to check against the token's 'iss' claim.</returns>
public IList<string> ValidIssuers =>
_validIssuers ??
Interlocked.CompareExchange(ref _validIssuers, [], null) ??
_validIssuers;

/// <summary>
/// Gets the <see cref="IList{String}"/> that contains valid types that will be used to check against the JWT header's 'typ' claim.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ namespace Microsoft.IdentityModel.Tokens
/// </summary>
/// <param name="issuer">The issuer to validate.</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> that is being validated.</param>
/// <param name="validationParameters">The <see cref="TokenValidationParameters"/> to be used for validating the token.</param>
/// <param name="validationParameters">The <see cref="ValidationParameters"/> to be used for validating the token.</param>
/// <param name="callContext"></param>
/// <param name="cancellationToken"></param>
/// <returns>A <see cref="IssuerValidationResult"/>that contains the results of validating the issuer.</returns>
/// <returns>An <see cref="IssuerValidationResult"/>that contains the results of validating the issuer.</returns>
/// <remarks>This delegate is not expected to throw.</remarks>
internal delegate Task<IssuerValidationResult> IssuerValidationDelegateAsync(
string issuer,
SecurityToken securityToken,
TokenValidationParameters validationParameters,
ValidationParameters validationParameters,
CallContext callContext,
CancellationToken cancellationToken);

Expand All @@ -37,19 +37,15 @@ public static partial class Validators
/// </summary>
/// <param name="issuer">The issuer to validate</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> that is being validated.</param>
/// <param name="validationParameters">The <see cref="TokenValidationParameters"/> to be used for validating the token.</param>
/// <param name="validationParameters">The <see cref="ValidationParameters"/> to be used for validating the token.</param>
/// <param name="callContext"></param>
/// <param name="cancellationToken"></param>
/// <returns>The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity".</returns>
/// <exception cref="ArgumentNullException">If 'validationParameters' is null.</exception>
/// <exception cref="ArgumentNullException">If 'issuer' is null or whitespace and <see cref="TokenValidationParameters.ValidateIssuer"/> is true.</exception>
/// <exception cref="SecurityTokenInvalidIssuerException">If <see cref="TokenValidationParameters.ValidIssuer"/> is null or whitespace and <see cref="TokenValidationParameters.ValidIssuers"/> is null.</exception>
/// <exception cref="SecurityTokenInvalidIssuerException">If 'issuer' failed to matched either <see cref="TokenValidationParameters.ValidIssuer"/> or one of <see cref="TokenValidationParameters.ValidIssuers"/>.</exception>
/// <returns>An <see cref="IssuerValidationResult"/>that contains the results of validating the issuer.</returns>
/// <remarks>An EXACT match is required.</remarks>
internal static async Task<IssuerValidationResult> ValidateIssuerAsync(
string issuer,
SecurityToken securityToken,
TokenValidationParameters validationParameters,
ValidationParameters validationParameters,
CallContext callContext,
CancellationToken cancellationToken)
{
Expand Down Expand Up @@ -95,10 +91,8 @@ internal static async Task<IssuerValidationResult> 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,
Expand Down Expand Up @@ -126,25 +120,19 @@ internal static async Task<IssuerValidationResult> 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);

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));
Expand All @@ -160,9 +148,8 @@ internal static async Task<IssuerValidationResult> 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),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public static TheoryData<IssuerValidatorTheoryData> AsyncIssuerValidatorTestCase
theoryData.Add(new IssuerValidatorTheoryData
{
Issuer = null,
ValidationParameters = new TokenValidationParameters(),
ValidationParameters = new ValidationParameters(),
});

return theoryData;
Expand All @@ -52,7 +52,7 @@ public static TheoryData<IssuerValidatorTheoryData> 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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -56,26 +52,8 @@ public static TheoryData<IssuerValidationResultsTheoryData> 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")
{
Expand All @@ -94,7 +72,7 @@ public static TheoryData<IssuerValidationResultsTheoryData> IssuerValdationResul
new StackFrame(true))),
IsValid = false,
SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim),
ValidationParameters = new TokenValidationParameters(),
ValidationParameters = new ValidationParameters()
});

theoryData.Add(new IssuerValidationResultsTheoryData("NULL_ValidationParameters")
Expand Down Expand Up @@ -132,10 +110,9 @@ public static TheoryData<IssuerValidationResultsTheoryData> 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,
Expand All @@ -145,40 +122,44 @@ public static TheoryData<IssuerValidationResultsTheoryData> IssuerValdationResul
IssuerValidationResult.ValidationSource.IssuerIsConfigurationIssuer),
IsValid = true,
SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim),
ValidationParameters = new TokenValidationParameters()
ValidationParameters = new ValidationParameters()
{
ConfigurationManager = new MockConfigurationManager<OpenIdConnectConfiguration>(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;
Expand All @@ -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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public void SetValidators_NullValue_ThrowsArgumentNullException()
Assert.Throws<ArgumentNullException>(() => validationParameters.AudienceValidator = null);
}

[Fact]
public void ValidIssuers_GetReturnsEmptyList()
{
var validationParameters = new ValidationParameters();

Assert.Equal(0, validationParameters.ValidIssuers.Count);
}

[Fact]
public void ValidTypes_Get_ReturnsValidTokenTypes()
{
Expand Down
Loading