Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ public class AadIssuerValidator
internal const string V2EndpointSuffixWithTrailingSlash = $"{V2EndpointSuffix}/";
internal const string TenantIdTemplate = "{tenantid}";

private Func<string, BaseConfigurationManager> _configurationManagerProvider;

internal AadIssuerValidator(
HttpClient httpClient,
string aadAuthority)
Expand All @@ -36,11 +38,25 @@ internal AadIssuerValidator(
IsV2Authority = aadAuthority.Contains(V2EndpointSuffix);
}

internal AadIssuerValidator(
HttpClient httpClient,
Comment thread
MZOLN marked this conversation as resolved.
string aadAuthority,
Func<string, BaseConfigurationManager> configurationManagerProvider)
: this(httpClient, aadAuthority)
{
if (configurationManagerProvider == null)
throw new ArgumentNullException(nameof(configurationManagerProvider));

_configurationManagerProvider = configurationManagerProvider;
Comment thread
MZOLN marked this conversation as resolved.
}

private HttpClient HttpClient { get; }
private string _aadAuthorityV1;
private string _aadAuthorityV2;
private BaseConfigurationManager _configurationManagerV1;
private BaseConfigurationManager _configurationManagerV2;
private IssuerLastKnownGood _issuerLKGV1;
private IssuerLastKnownGood _issuerLKGV2;

internal BaseConfigurationManager ConfigurationManagerV1
{
Expand Down Expand Up @@ -180,25 +196,30 @@ internal async ValueTask<string> ValidateAsync(

try
{
BaseConfigurationManager effectiveConfigurationManager = GetEffectiveConfigurationManager(securityToken);
if (validationParameters.RefreshBeforeValidation)
effectiveConfigurationManager.RequestRefresh();
var isV2Issuer = IsV2Issuer(securityToken);
var effectiveConfigurationManager = GetEffectiveConfigurationManager(isV2Issuer);
Comment thread
MZOLN marked this conversation as resolved.

BaseConfiguration configuration = await effectiveConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false);
string aadIssuer = configuration.Issuer;

if (!validationParameters.ValidateWithLKG)
string aadIssuer = null;
if (validationParameters.ValidateWithLKG)
{
if (IsValidIssuer(aadIssuer, tenantId, issuer))
{
effectiveConfigurationManager.LastKnownGoodConfiguration = new OpenIdConnectConfiguration() { Issuer = aadIssuer };
return issuer;
}
// returns null if LKG issuer expired
aadIssuer = GetEffectiveLKGIssuer(isV2Issuer);
}
else
{
if (effectiveConfigurationManager.LastKnownGoodConfiguration != null &&
IsValidIssuer(effectiveConfigurationManager.LastKnownGoodConfiguration.Issuer, tenantId, issuer))
var baseConfiguration = await GetBaseConfiguration(effectiveConfigurationManager, validationParameters).ConfigureAwait(false);
aadIssuer = baseConfiguration.Issuer;
}

if (aadIssuer != null)
{
var isIssuerValid = IsValidIssuer(aadIssuer, tenantId, issuer);
Comment thread
MZOLN marked this conversation as resolved.

// The original LKG assignment behavior for previous self-state management.
if (isIssuerValid && !validationParameters.ValidateWithLKG)
SetEffectiveLKGIssuer(aadIssuer, isV2Issuer, effectiveConfigurationManager.LastKnownGoodLifetime);

if (isIssuerValid)
return issuer;
}
}
Expand Down Expand Up @@ -233,17 +254,7 @@ internal async ValueTask<string> ValidateAsync(
/// <exception cref="ArgumentNullException">if <paramref name="aadAuthority"/> is null or empty.</exception>
public static AadIssuerValidator GetAadIssuerValidator(string aadAuthority, HttpClient httpClient)
{
if(string.IsNullOrEmpty(aadAuthority))
throw LogHelper.LogArgumentNullException(nameof(aadAuthority));

if (s_issuerValidators.TryGetValue(aadAuthority, out AadIssuerValidator aadIssuerValidator))
return aadIssuerValidator;

s_issuerValidators[aadAuthority] = new AadIssuerValidator(
httpClient,
aadAuthority);

return s_issuerValidators[aadAuthority];
return GetAadIssuerValidator(aadAuthority, httpClient, null);
}

/// <summary>
Expand All @@ -258,7 +269,40 @@ public static AadIssuerValidator GetAadIssuerValidator(string aadAuthority, Http
/// <exception cref="ArgumentNullException">if <paramref name="aadAuthority"/> is null or empty.</exception>
public static AadIssuerValidator GetAadIssuerValidator(string aadAuthority)
{
return GetAadIssuerValidator(aadAuthority, null);
return GetAadIssuerValidator(aadAuthority, null, null);
}

/// <summary>
/// Gets an <see cref="AadIssuerValidator"/> for an Azure Active Directory (AAD) authority.
/// </summary>
/// <param name="aadAuthority">The authority to create the validator for, e.g. https://login.microsoftonline.com/. </param>
/// <param name="httpClient">Optional HttpClient to use to retrieve the endpoint metadata (can be null).</param>
/// <param name="configurationManagerProvider">Configuration manager provider. Injection point for metadata managed outside of the class.</param>
/// <example><code>
/// AadIssuerValidator aadIssuerValidator = AadIssuerValidator.GetAadIssuerValidator(authority, configurationManagerProvider);
/// TokenValidationParameters.IssuerValidator = aadIssuerValidator.Validate;
/// </code></example>
/// <returns>A <see cref="AadIssuerValidator"/> for the aadAuthority.</returns>
/// <exception cref="ArgumentNullException">if <paramref name="aadAuthority"/> is null or empty.</exception>
internal static AadIssuerValidator GetAadIssuerValidator(string aadAuthority, HttpClient httpClient, Func<string, BaseConfigurationManager> configurationManagerProvider)
{
if (string.IsNullOrEmpty(aadAuthority))
throw LogHelper.LogArgumentNullException(nameof(aadAuthority));

if (configurationManagerProvider != null)
return new AadIssuerValidator(
httpClient,
aadAuthority,
configurationManagerProvider);

if (s_issuerValidators.TryGetValue(aadAuthority, out AadIssuerValidator aadIssuerValidator))
return aadIssuerValidator;

s_issuerValidators[aadAuthority] = new AadIssuerValidator(
Comment thread
MZOLN marked this conversation as resolved.
httpClient,
aadAuthority);

return s_issuerValidators[aadAuthority];
}

private static string CreateV1Authority(string aadV2Authority)
Expand Down Expand Up @@ -306,11 +350,58 @@ private static bool IsValidIssuer(string validIssuerTemplate, string tenantId, s
}
}

private BaseConfigurationManager GetEffectiveConfigurationManager(SecurityToken securityToken)
private void SetEffectiveLKGIssuer(string aadIssuer, bool isV2Issuer, TimeSpan lastKnownGoodLifetime)
{
var isV2 = securityToken.Issuer.EndsWith(V2EndpointSuffixWithTrailingSlash, StringComparison.OrdinalIgnoreCase) ||
var issuerLKG = new IssuerLastKnownGood
{
Issuer = aadIssuer,
LastKnownGoodLifetime = lastKnownGoodLifetime
};

if (isV2Issuer)
_issuerLKGV2 = issuerLKG;
else
_issuerLKGV1 = issuerLKG;
}

private string GetEffectiveLKGIssuer(bool isV2Issuer)
{
var effectiveLKGIssuer = isV2Issuer ? _issuerLKGV2 : _issuerLKGV1;
if (effectiveLKGIssuer != null && effectiveLKGIssuer.IsValid)
{
return effectiveLKGIssuer.Issuer;
}

return null;
}

private static bool IsV2Issuer(SecurityToken securityToken)
{
return securityToken.Issuer.EndsWith(V2EndpointSuffixWithTrailingSlash, StringComparison.OrdinalIgnoreCase) ||
securityToken.Issuer.EndsWith(V2EndpointSuffix, StringComparison.OrdinalIgnoreCase);
return isV2 ? ConfigurationManagerV2 : ConfigurationManagerV1;
}

private BaseConfigurationManager GetEffectiveConfigurationManager(bool isV2Issuer)
{
if (_configurationManagerProvider != null)
{
var aadAuthority = isV2Issuer ? AadAuthorityV2 : AadAuthorityV1;
var configurationManager = _configurationManagerProvider(aadAuthority);

if (configurationManager != null)
Comment thread
MZOLN marked this conversation as resolved.
return configurationManager;
}

// If no provider or provider returned null, fallback to previous strategy
return isV2Issuer ? ConfigurationManagerV2 : ConfigurationManagerV1;
}

private static async Task<BaseConfiguration> GetBaseConfiguration(BaseConfigurationManager configurationManager, TokenValidationParameters validationParameters)
{
if (validationParameters.RefreshBeforeValidation)
configurationManager.RequestRefresh();

return await configurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false);
}

/// <summary>Gets the tenant ID from a token.</summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Validators
{
/// <summary>
/// Class representing the last known good for issuer.
/// </summary>
internal class IssuerLastKnownGood
{
private string _issuer;
private TimeSpan _lastKnownGoodLifetime;
private DateTime? _lastKnownGoodConfigFirstUse = null;

/// <summary>
/// Gets or sets the issuer value.
/// </summary>
public string Issuer
{
get
{
return _issuer;
}
set
{
if (value == null)
throw LogHelper.LogArgumentNullException(nameof(value));

_lastKnownGoodConfigFirstUse = DateTime.UtcNow;
_issuer = value;
}
}

/// <summary>
/// Gets or sets the last known good lifetime.
/// </summary>
public TimeSpan LastKnownGoodLifetime
{
get { return _lastKnownGoodLifetime; }
set
{
if (value < TimeSpan.Zero)
throw LogHelper.LogExceptionMessage(new ArgumentOutOfRangeException(nameof(value), LogHelper.FormatInvariant(LogMessages.IDX40008, value)));

_lastKnownGoodLifetime = value;
}
}

/// <summary>
/// Gets an indicator whether the value is still within its lifetime and is valid.
/// </summary>
public bool IsValid
{
get
{
return _lastKnownGoodConfigFirstUse + LastKnownGoodLifetime > DateTime.UtcNow;
Comment thread
MZOLN marked this conversation as resolved.
}
}

}
}
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Validators/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ internal static class LogMessages
public const string IDX40004 = "IDX40004: Token issuer: '{0}', does not contain the `tid` or `tenantId` claim present in the token: '{1}'.";
public const string IDX40005 = "IDX40005: Token issuer: '{0}', does not match the signing key issuer: '{1}'.";
public const string IDX40007 = "IDX40007: RequireSignedTokens property on ValidationParameters is set to true, but the issuer signing key is null.";
public const string IDX40008 = "IDX40008: When setting LastKnownGoodLifetime, the value must be greater than or equal to zero. value: '{0}'.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.IdentityModel.Tokens.Jwt.Tests;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Security.Claims;
using System.Security.Cryptography;
Expand Down Expand Up @@ -3474,6 +3475,13 @@ public async Task ValidateJWSWithConfigAsync(JwtTheoryData theoryData)
var context = TestUtilities.WriteHeader($"{this}.ValidateJWSWithConfigAsync", theoryData);
try
{
// clear up static state.
AadIssuerValidator.s_issuerValidators[Default.AadV1Authority] = new AadIssuerValidator(null, Default.AadV1Authority);

// previous instance is captured in a closure during theorydata set setup.
if (theoryData.ValidationParameters.IssuerValidator != null)
theoryData.ValidationParameters.IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).Validate;

var handler = new JsonWebTokenHandler();
var jwt = handler.ReadJsonWebToken(theoryData.Token);
AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = theoryData.ValidationParameters.ConfigurationManager;
Expand Down Expand Up @@ -3539,7 +3547,36 @@ public void ValidateJWSWithLastKnownGood(JwtTheoryData theoryData)
var context = TestUtilities.WriteHeader($"{this}.ValidateJWSWithLastKnownGood", theoryData);
try
{
// clear up static state.
AadIssuerValidator.s_issuerValidators[Default.AadV1Authority] = new AadIssuerValidator(null, Default.AadV1Authority);

// previous instance is captured in a closure during theorydata set setup.
if (theoryData.ValidationParameters.IssuerValidator != null)
theoryData.ValidationParameters.IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).Validate;

var handler = new JsonWebTokenHandler();

if (theoryData.SetupIssuerLkg)
{
// make a valid pass to initiate issuer LKG.
var issuerValidator = AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority);
issuerValidator.ConfigurationManagerV1 = theoryData.SetupIssuerLkgConfigurationManager;

var previousValidateWithLKG = theoryData.ValidationParameters.ValidateWithLKG;
theoryData.ValidationParameters.ValidateWithLKG = false;

var setupValidationResult = handler.ValidateTokenAsync(theoryData.Token, theoryData.ValidationParameters).Result;

theoryData.ValidationParameters.ValidateWithLKG = previousValidateWithLKG;

if (setupValidationResult.Exception != null)
{
if (setupValidationResult.IsValid)
context.AddDiff("setupValidationResult.IsValid, setupValidationResult.Exception != null");
throw setupValidationResult.Exception;
}
}

AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = theoryData.ValidationParameters.ConfigurationManager;
var validationResult = handler.ValidateTokenAsync(theoryData.Token, theoryData.ValidationParameters).Result;
if (validationResult.Exception != null)
Expand Down Expand Up @@ -3568,7 +3605,35 @@ public void ValidateJWEWithLastKnownGood(JwtTheoryData theoryData)
var context = TestUtilities.WriteHeader($"{this}.ValidateJWEWithLastKnownGood", theoryData);
try
{
// clear up static state.
AadIssuerValidator.s_issuerValidators[Default.AadV1Authority] = new AadIssuerValidator(null, Default.AadV1Authority);

// previous instance is captured in a closure during theorydata set setup.
if (theoryData.ValidationParameters.IssuerValidator != null)
theoryData.ValidationParameters.IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).Validate;

var handler = new JsonWebTokenHandler();
if (theoryData.SetupIssuerLkg)
{
// make a valid pass to initiate issuer LKG.
var issuerValidator = AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority);
issuerValidator.ConfigurationManagerV1 = theoryData.SetupIssuerLkgConfigurationManager;

var previousValidateWithLKG = theoryData.ValidationParameters.ValidateWithLKG;
theoryData.ValidationParameters.ValidateWithLKG = false;

var setupValidationResult = handler.ValidateTokenAsync(theoryData.Token, theoryData.ValidationParameters).Result;

theoryData.ValidationParameters.ValidateWithLKG = previousValidateWithLKG;

if (setupValidationResult.Exception != null)
{
if (setupValidationResult.IsValid)
context.AddDiff("setupValidationResult.IsValid, setupValidationResult.Exception != null");
throw setupValidationResult.Exception;
}
}

AadIssuerValidator.GetAadIssuerValidator(Default.AadV1Authority).ConfigurationManagerV1 = theoryData.ValidationParameters.ConfigurationManager;
var validationResult = handler.ValidateTokenAsync(theoryData.Token, theoryData.ValidationParameters).Result;
if (validationResult.Exception != null)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ public class MockConfigurationManager<T> : BaseConfigurationManager, IConfigurat
private bool _firstGet = true;
private Exception _exToThrowOnFirstGet;

/// <summary>
/// Gets or sets the refreshed configuration. Use with RequestRefresh to simulate data transmission.
/// </summary>
public T RefreshedConfiguration
{
get { return _refreshedConfiguration; }
set { _refreshedConfiguration = value; }
}

/// <summary>
/// Initializes an new instance of <see cref="MockConfigurationManager{T}"/> with a Configuration instance.
/// </summary>
Expand Down
Loading