Skip to content

Commit

Permalink
make aadissuervalidator a singleton (#675)
Browse files Browse the repository at this point in the history
  • Loading branch information
jennyf19 authored Oct 12, 2020
1 parent 3411f8a commit a239329
Show file tree
Hide file tree
Showing 9 changed files with 111 additions and 68 deletions.
1 change: 1 addition & 0 deletions src/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ public override void OnException(ExceptionContext context)
return null;
}
}

private static bool IsAjaxRequest(HttpRequest request)
{
return string.Equals(request.Query[Constants.XRequestedWith], Constants.XmlHttpRequest, StringComparison.Ordinal) ||
Expand Down
21 changes: 13 additions & 8 deletions src/Microsoft.Identity.Web/Microsoft.Identity.Web.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 1 addition & 45 deletions src/Microsoft.Identity.Web/Resource/AadIssuerValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
// Licensed under the MIT License.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using Microsoft.Identity.Web.InstanceDiscovery;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.Identity.Web.Resource
Expand All @@ -19,56 +15,16 @@ namespace Microsoft.Identity.Web.Resource
/// </summary>
public class AadIssuerValidator
{
// TODO: separate AadIssuerValidator creation logic from the validation logic in order to unit test it
private static readonly IDictionary<string, AadIssuerValidator> s_issuerValidators = new ConcurrentDictionary<string, AadIssuerValidator>();

private static readonly ConfigurationManager<IssuerMetadata> s_configManager = new ConfigurationManager<IssuerMetadata>(Constants.AzureADIssuerMetadataUrl, new IssuerConfigurationRetriever());

/// <summary>
/// A list of all Issuers across the various Azure AD instances.
/// </summary>
private readonly ISet<string> _issuerAliases;

internal /* internal for test */ AadIssuerValidator(IEnumerable<string> aliases)
internal /*internal for tests*/ AadIssuerValidator(IEnumerable<string> aliases)
{
_issuerAliases = new HashSet<string>(aliases, StringComparer.OrdinalIgnoreCase);
}

/// <summary>
/// Gets an <see cref="AadIssuerValidator"/> for an authority.
/// </summary>
/// <param name="aadAuthority">The authority to create the validator for, e.g. https://login.microsoftonline.com/. </param>
/// <returns>A <see cref="AadIssuerValidator"/> for the aadAuthority.</returns>
/// <exception cref="ArgumentNullException">if <paramref name="aadAuthority"/> is null or empty.</exception>
public static AadIssuerValidator GetIssuerValidator(string aadAuthority)
{
if (string.IsNullOrEmpty(aadAuthority))
{
throw new ArgumentNullException(nameof(aadAuthority));
}

Uri.TryCreate(aadAuthority, UriKind.Absolute, out Uri? authorityUri);
string authorityHost = authorityUri?.Authority ?? new Uri(Constants.FallbackAuthority).Authority;

if (s_issuerValidators.TryGetValue(authorityHost, out AadIssuerValidator? aadIssuerValidator))
{
return aadIssuerValidator;
}

// In the constructor, we hit the Azure AD issuer metadata endpoint and cache the aliases. The data is cached for 24 hrs.
IssuerMetadata issuerMetadata = s_configManager.GetConfigurationAsync().ConfigureAwait(false).GetAwaiter().GetResult();

// Add issuer aliases of the chosen authority to the cache
IEnumerable<string> aliases = issuerMetadata.Metadata
.Where(m => m.Aliases.Any(a => string.Equals(a, authorityHost, StringComparison.OrdinalIgnoreCase)))
.SelectMany(m => m.Aliases)
.Append(authorityHost) // For B2C scenarios, the alias will be the authority itself
.Distinct();
s_issuerValidators[authorityHost] = new AadIssuerValidator(aliases);

return s_issuerValidators[authorityHost];
}

/// <summary>
/// Validate the issuer for multi-tenant applications of various audiences (Work and School accounts, or Work and School accounts +
/// Personal accounts).
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Identity.Web.InstanceDiscovery;
using Microsoft.IdentityModel.Protocols;

namespace Microsoft.Identity.Web.Resource
{
/// <summary>
/// Factory class for creating the IssuerValidator per authority.
/// </summary>
internal class MicrosoftIdentityIssuerValidatorFactory
{
private readonly IDictionary<string, AadIssuerValidator> _issuerValidators = new ConcurrentDictionary<string, AadIssuerValidator>();

private readonly ConfigurationManager<IssuerMetadata> _configManager = new ConfigurationManager<IssuerMetadata>(Constants.AzureADIssuerMetadataUrl, new IssuerConfigurationRetriever());

/// <summary>
/// Gets an <see cref="AadIssuerValidator"/> for an authority.
/// </summary>
/// <param name="aadAuthority">The authority to create the validator for, e.g. https://login.microsoftonline.com/. </param>
/// <returns>A <see cref="AadIssuerValidator"/> for the aadAuthority.</returns>
/// <exception cref="ArgumentNullException">if <paramref name="aadAuthority"/> is null or empty.</exception>
public AadIssuerValidator GetAadIssuerValidator(string aadAuthority)
{
if (string.IsNullOrEmpty(aadAuthority))
{
throw new ArgumentNullException(nameof(aadAuthority));
}

Uri.TryCreate(aadAuthority, UriKind.Absolute, out Uri? authorityUri);
string authorityHost = authorityUri?.Authority ?? new Uri(Constants.FallbackAuthority).Authority;

if (_issuerValidators.TryGetValue(authorityHost, out AadIssuerValidator? aadIssuerValidator))
{
return aadIssuerValidator;
}

// In the constructor, we hit the Azure AD issuer metadata endpoint and cache the aliases. The data is cached for 24 hrs.
IssuerMetadata issuerMetadata = _configManager.GetConfigurationAsync().ConfigureAwait(false).GetAwaiter().GetResult();

// Add issuer aliases of the chosen authority to the cache
IEnumerable<string> aliases = issuerMetadata.Metadata
.Where(m => m.Aliases.Any(a => string.Equals(a, authorityHost, StringComparison.OrdinalIgnoreCase)))
.SelectMany(m => m.Aliases)
.Append(authorityHost) // For B2C scenarios, the alias will be the authority itself
.Distinct();
_issuerValidators[authorityHost] = new AadIssuerValidator(aliases);

return _issuerValidators[authorityHost];
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ private static void AddMicrosoftIdentityWebApiImplementation(
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<MicrosoftIdentityOptions>, MicrosoftIdentityOptionsValidation>());
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient();
builder.Services.TryAddSingleton<MicrosoftIdentityIssuerValidatorFactory>();

if (subscribeToJwtBearerMiddlewareDiagnosticsEvents)
{
Expand Down Expand Up @@ -194,7 +195,11 @@ private static void AddMicrosoftIdentityWebApiImplementation(
{
// Instead of using the default validation (validating against a single tenant, as we do in line of business apps),
// we inject our own multi-tenant validation logic (which even accepts both v1.0 and v2.0 tokens)
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory =
serviceProvider.GetRequiredService<MicrosoftIdentityIssuerValidatorFactory>();

options.TokenValidationParameters.IssuerValidator =
microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate;
}

// If you provide a token decryption certificate, it will be used to decrypt the token
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ private static void AddMicrosoftIdentityWebAppInternal(

builder.AddCookie(cookieScheme, configureCookieAuthenticationOptions);

builder.Services.TryAddSingleton<MicrosoftIdentityIssuerValidatorFactory>();

if (subscribeToOpenIdConnectMiddlewareDiagnosticsEvents)
{
builder.Services.AddSingleton<IOpenIdConnectMiddlewareDiagnostics, OpenIdConnectMiddlewareDiagnostics>();
Expand Down Expand Up @@ -267,7 +269,11 @@ private static void AddMicrosoftIdentityWebAppInternal(
// If you want to restrict the users that can sign-in to several organizations
// Set the tenant value in the appsettings.json file to 'organizations', and add the
// issuers you want to accept to options.TokenValidationParameters.ValidIssuers collection
options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetIssuerValidator(options.Authority).Validate;
MicrosoftIdentityIssuerValidatorFactory microsoftIdentityIssuerValidatorFactory =
serviceProvider.GetRequiredService<MicrosoftIdentityIssuerValidatorFactory>();

options.TokenValidationParameters.IssuerValidator =
microsoftIdentityIssuerValidatorFactory.GetAadIssuerValidator(options.Authority).Validate;
}

// Avoids having users being presented the select account dialog when they are already signed-in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,25 @@ namespace Microsoft.Identity.Web.Test.Resource
{
public class AadIssuerValidatorTests
{
private readonly MicrosoftIdentityIssuerValidatorFactory _issuerValidatorFactory;

public AadIssuerValidatorTests()
{
_issuerValidatorFactory = new MicrosoftIdentityIssuerValidatorFactory();
}

[Fact]
public void GetIssuerValidator_NullOrEmptyAuthority_ThrowsException()
{
var exception = Assert.Throws<ArgumentNullException>(TestConstants.AadAuthority, () => AadIssuerValidator.GetIssuerValidator(string.Empty));
var exception = Assert.Throws<ArgumentNullException>(TestConstants.AadAuthority, () => _issuerValidatorFactory.GetAadIssuerValidator(string.Empty));

exception = Assert.Throws<ArgumentNullException>(TestConstants.AadAuthority, () => AadIssuerValidator.GetIssuerValidator(null));
exception = Assert.Throws<ArgumentNullException>(TestConstants.AadAuthority, () => _issuerValidatorFactory.GetAadIssuerValidator(null));
}

[Fact]
public void GetIssuerValidator_InvalidAuthority_ReturnsValidatorBasedOnFallbackAuthority()
{
var validator = AadIssuerValidator.GetIssuerValidator(TestConstants.InvalidAuthorityFormat);
var validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.InvalidAuthorityFormat);

Assert.NotNull(validator);
}
Expand All @@ -37,7 +44,7 @@ public void GetIssuerValidator_AuthorityInAliases_ReturnsValidator()
{
var authorityInAliases = TestConstants.AuthorityCommonTenantWithV2;

var validator = AadIssuerValidator.GetIssuerValidator(authorityInAliases);
var validator = _issuerValidatorFactory.GetAadIssuerValidator(authorityInAliases);

Assert.NotNull(validator);
}
Expand All @@ -47,7 +54,7 @@ public void GetIssuerValidator_B2cAuthorityNotInAliases_ReturnsValidator()
{
var authorityNotInAliases = TestConstants.B2CAuthorityWithV2;

var validator = AadIssuerValidator.GetIssuerValidator(authorityNotInAliases);
var validator = _issuerValidatorFactory.GetAadIssuerValidator(authorityNotInAliases);

Assert.NotNull(validator);
}
Expand All @@ -57,8 +64,8 @@ public void GetIssuerValidator_CachedAuthority_ReturnsCachedValidator()
{
var authorityNotInAliases = TestConstants.AuthorityWithTenantSpecifiedWithV2;

var validator1 = AadIssuerValidator.GetIssuerValidator(authorityNotInAliases);
var validator2 = AadIssuerValidator.GetIssuerValidator(authorityNotInAliases);
var validator1 = _issuerValidatorFactory.GetAadIssuerValidator(authorityNotInAliases);
var validator2 = _issuerValidatorFactory.GetAadIssuerValidator(authorityNotInAliases);

Assert.Same(validator1, validator2);
}
Expand Down Expand Up @@ -250,7 +257,7 @@ public void Validate_FromB2CAuthority_WithNoTidClaim_ValidateSuccessfully()
Claim tfpClaim = new Claim(TestConstants.ClaimNameTfp, TestConstants.B2CSignUpSignInUserFlow);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: TestConstants.B2CIssuer, claims: new[] { issClaim, tfpClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CAuthorityWithV2);

validator.Validate(
TestConstants.B2CIssuer,
Expand All @@ -271,7 +278,7 @@ public void Validate_FromB2CAuthority_WithTidClaim_ValidateSuccessfully()
Claim tidClaim = new Claim(TestConstants.ClaimNameTid, TestConstants.B2CTenantAsGuid);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: TestConstants.B2CIssuer, claims: new[] { issClaim, tfpClaim, tidClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CAuthorityWithV2);

validator.Validate(
TestConstants.B2CIssuer,
Expand All @@ -291,7 +298,7 @@ public void Validate_FromB2CAuthority_InvalidIssuer_Fails()
Claim tfpClaim = new Claim(TestConstants.ClaimNameTfp, TestConstants.B2CSignUpSignInUserFlow);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: TestConstants.B2CIssuer2, claims: new[] { issClaim, tfpClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CAuthorityWithV2);

Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
validator.Validate(
Expand All @@ -313,7 +320,7 @@ public void Validate_FromB2CAuthority_InvalidIssuerTid_Fails()
Claim tfpClaim = new Claim(TestConstants.ClaimNameTfp, TestConstants.B2CSignUpSignInUserFlow);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: issuerWithInvalidTid, claims: new[] { issClaim, tfpClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CAuthorityWithV2);

Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
validator.Validate(
Expand All @@ -334,7 +341,7 @@ public void Validate_FromCustomB2CAuthority_ValidateSuccessfully()
Claim tfpClaim = new Claim(TestConstants.ClaimNameTfp, TestConstants.B2CSignUpSignInUserFlow);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: TestConstants.B2CCustomDomainIssuer, claims: new[] { issClaim, tfpClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CCustomDomainAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CCustomDomainAuthorityWithV2);

validator.Validate(
TestConstants.B2CCustomDomainIssuer,
Expand All @@ -351,7 +358,7 @@ public void Validate_FromB2CAuthority_WithTfpIssuer_ThrowsException()
Claim issClaim = new Claim(TestConstants.ClaimNameIss, TestConstants.B2CIssuerTfp);
JwtSecurityToken jwtSecurityToken = new JwtSecurityToken(issuer: TestConstants.B2CIssuerTfp, claims: new[] { issClaim });

AadIssuerValidator validator = AadIssuerValidator.GetIssuerValidator(TestConstants.B2CAuthorityWithV2);
AadIssuerValidator validator = _issuerValidatorFactory.GetAadIssuerValidator(TestConstants.B2CAuthorityWithV2);

var exception = Assert.Throws<SecurityTokenInvalidIssuerException>(() =>
validator.Validate(
Expand Down
Loading

0 comments on commit a239329

Please sign in to comment.