-
Notifications
You must be signed in to change notification settings - Fork 218
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
updates to id web to support authentication handlers than JwtBearer #1493
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,8 +4,8 @@ | |
using System; | ||
using System.Globalization; | ||
using System.IdentityModel.Tokens.Jwt; | ||
using System.Linq; | ||
using System.Net.Http; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Identity.Web.InstanceDiscovery; | ||
using Microsoft.IdentityModel.JsonWebTokens; | ||
using Microsoft.IdentityModel.Protocols; | ||
|
@@ -19,17 +19,14 @@ namespace Microsoft.Identity.Web.Resource | |
public class AadIssuerValidator | ||
{ | ||
internal AadIssuerValidator( | ||
IOptions<AadIssuerValidatorOptions> aadIssuerValidatorOptions, | ||
IHttpClientFactory httpClientFactory, | ||
HttpClient? httpClient, | ||
string aadAuthority) | ||
{ | ||
AadIssuerValidatorOptions = aadIssuerValidatorOptions; | ||
HttpClientFactory = httpClientFactory; | ||
HttpClient = httpClient; | ||
AadAuthority = aadAuthority.TrimEnd('/'); | ||
} | ||
|
||
private IOptions<AadIssuerValidatorOptions> AadIssuerValidatorOptions { get; } | ||
private IHttpClientFactory HttpClientFactory { get; } | ||
private HttpClient? HttpClient { get; } | ||
internal string? AadIssuerV1 { get; set; } | ||
internal string? AadIssuerV2 { get; set; } | ||
internal string AadAuthority { get; set; } | ||
|
@@ -71,7 +68,7 @@ public string Validate( | |
string tenantId = GetTenantIdFromToken(securityToken); | ||
if (string.IsNullOrWhiteSpace(tenantId)) | ||
{ | ||
throw new SecurityTokenInvalidIssuerException(IDWebErrorMessage.TenantIdClaimNotPresentInToken); | ||
throw new SecurityTokenInvalidIssuerException(IssuerValidatorErrorMessage.TenantIdClaimNotPresentInToken); | ||
} | ||
|
||
if (validationParameters.ValidIssuers != null) | ||
|
@@ -132,36 +129,46 @@ public string Validate( | |
throw new SecurityTokenInvalidIssuerException( | ||
string.Format( | ||
CultureInfo.InvariantCulture, | ||
IDWebErrorMessage.IssuerDoesNotMatchValidIssuers, | ||
IssuerValidatorErrorMessage.IssuerDoesNotMatchValidIssuers, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would be good to throw in the catch segment above and capture more information on what caused a potential issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. good idea. will update. |
||
actualIssuer)); | ||
} | ||
|
||
private string CreateV1Authority() | ||
{ | ||
if (AadAuthority.Contains(Constants.Organizations, StringComparison.OrdinalIgnoreCase)) | ||
#if DOTNET_STANDARD_20 || DOTNET_462 || DOTNET_472 | ||
if (AadAuthority.Contains(IssuerValidatorConstants.Organizations)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would it be fair to say that if AadAuthority is not 'common'/'organizations', extracting a tenantId and creating & comparing URIs is not necessary? Thinking if we could cut these cycles as issuer validation is on the hot-path. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense, great point. |
||
{ | ||
return AadAuthority.Replace($"{Constants.Organizations}/v2.0", Constants.Common, StringComparison.OrdinalIgnoreCase); | ||
return AadAuthority.Replace($"{IssuerValidatorConstants.Organizations}/v2.0", IssuerValidatorConstants.Common); | ||
} | ||
|
||
return AadAuthority.Replace("/v2.0", string.Empty); | ||
#else | ||
|
||
if (AadAuthority.Contains(IssuerValidatorConstants.Organizations, StringComparison.OrdinalIgnoreCase)) | ||
{ | ||
return AadAuthority.Replace($"{IssuerValidatorConstants.Organizations}/v2.0", IssuerValidatorConstants.Common, StringComparison.OrdinalIgnoreCase); | ||
} | ||
|
||
return AadAuthority.Replace("/v2.0", string.Empty, StringComparison.OrdinalIgnoreCase); | ||
#endif | ||
} | ||
|
||
private ConfigurationManager<IssuerMetadata> CreateConfigManager( | ||
string aadAuthority) | ||
{ | ||
if (AadIssuerValidatorOptions?.Value?.HttpClientName != null && HttpClientFactory != null) | ||
if (HttpClient != null) | ||
{ | ||
return | ||
new ConfigurationManager<IssuerMetadata>( | ||
$"{aadAuthority}{Constants.OidcEndpoint}", | ||
$"{aadAuthority}{IssuerValidatorConstants.OidcEndpoint}", | ||
new IssuerConfigurationRetriever(), | ||
HttpClientFactory.CreateClient(AadIssuerValidatorOptions.Value.HttpClientName)); | ||
HttpClient); | ||
} | ||
else | ||
{ | ||
return | ||
new ConfigurationManager<IssuerMetadata>( | ||
$"{aadAuthority}{Constants.OidcEndpoint}", | ||
$"{aadAuthority}{IssuerValidatorConstants.OidcEndpoint}", | ||
new IssuerConfigurationRetriever()); | ||
} | ||
} | ||
|
@@ -175,7 +182,11 @@ private bool IsValidIssuer(string validIssuerTemplate, string tenantId, string a | |
|
||
try | ||
{ | ||
#if DOTNET_STANDARD_20 || DOTNET_462 || DOTNET_472 | ||
Uri issuerFromTemplateUri = new Uri(validIssuerTemplate.Replace("{tenantid}", tenantId)); | ||
#else | ||
Uri issuerFromTemplateUri = new Uri(validIssuerTemplate.Replace("{tenantid}", tenantId, StringComparison.OrdinalIgnoreCase)); | ||
#endif | ||
Uri actualIssuerUri = new Uri(actualIssuer); | ||
|
||
return issuerFromTemplateUri.AbsoluteUri == actualIssuerUri.AbsoluteUri; | ||
|
@@ -196,12 +207,12 @@ private static string GetTenantIdFromToken(SecurityToken securityToken) | |
{ | ||
if (securityToken is JwtSecurityToken jwtSecurityToken) | ||
{ | ||
if (jwtSecurityToken.Payload.TryGetValue(ClaimConstants.Tid, out object? tid)) | ||
if (jwtSecurityToken.Payload.TryGetValue(IssuerValidatorConstants.Tid, out object? tid)) | ||
{ | ||
return (string)tid; | ||
} | ||
|
||
jwtSecurityToken.Payload.TryGetValue(ClaimConstants.TenantId, out object? tenantId); | ||
jwtSecurityToken.Payload.TryGetValue(IssuerValidatorConstants.TenantId, out object? tenantId); | ||
if (tenantId != null) | ||
{ | ||
return (string)tenantId; | ||
|
@@ -213,13 +224,13 @@ private static string GetTenantIdFromToken(SecurityToken securityToken) | |
|
||
if (securityToken is JsonWebToken jsonWebToken) | ||
{ | ||
jsonWebToken.TryGetPayloadValue(ClaimConstants.Tid, out string? tid); | ||
jsonWebToken.TryGetPayloadValue(IssuerValidatorConstants.Tid, out string? tid); | ||
if (tid != null) | ||
{ | ||
return tid; | ||
} | ||
|
||
jsonWebToken.TryGetPayloadValue(ClaimConstants.TenantId, out string? tenantId); | ||
jsonWebToken.TryGetPayloadValue(IssuerValidatorConstants.TenantId, out string? tenantId); | ||
if (tenantId != null) | ||
{ | ||
return tenantId; | ||
|
@@ -251,9 +262,9 @@ private static string GetTenantIdFromIss(string iss) | |
return uri.Segments[1].TrimEnd('/'); | ||
} | ||
|
||
if (uri.Segments.Length == 5 && uri.Segments[1].TrimEnd('/') == ClaimConstants.Tfp) | ||
if (uri.Segments.Length == 5 && uri.Segments[1].TrimEnd('/') == IssuerValidatorConstants.Tfp) | ||
{ | ||
throw new SecurityTokenInvalidIssuerException(IDWebErrorMessage.B2CTfpIssuerNotSupported); | ||
throw new SecurityTokenInvalidIssuerException(IssuerValidatorErrorMessage.B2CTfpIssuerNotSupported); | ||
} | ||
|
||
return string.Empty; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
using System; | ||
using System.Collections.Concurrent; | ||
using System.Collections.Generic; | ||
using System.Net.Http; | ||
|
||
namespace Microsoft.Identity.Web.Resource | ||
{ | ||
/// <summary> | ||
/// Factory class for creating the AadIssuerValidator per authority. | ||
/// </summary> | ||
public class AadIssuerValidatorFactory | ||
{ | ||
/// <summary> | ||
/// Initializes a new instance of the <see cref="AadIssuerValidatorFactory"/> class. | ||
/// </summary> | ||
/// <param name="httpClient">Optional HttpClient to use to retrieve the endpoint metadata (can be null).</param> | ||
public AadIssuerValidatorFactory( | ||
HttpClient? httpClient = null) | ||
{ | ||
HttpClient = httpClient; | ||
} | ||
|
||
private readonly IDictionary<string, AadIssuerValidator> _issuerValidators = new ConcurrentDictionary<string, AadIssuerValidator>(); | ||
|
||
private HttpClient? HttpClient { get; } | ||
|
||
/// <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(IssuerValidatorConstants.FallbackAuthority).Authority; | ||
|
||
if (_issuerValidators.TryGetValue(authorityHost, out AadIssuerValidator? aadIssuerValidator)) | ||
{ | ||
return aadIssuerValidator; | ||
} | ||
|
||
_issuerValidators[authorityHost] = new AadIssuerValidator( | ||
HttpClient, | ||
aadAuthority); | ||
|
||
return _issuerValidators[authorityHost]; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
namespace Microsoft.Identity.Web | ||
{ | ||
/// <summary> | ||
/// General constants for Microsoft Identity Web Issuer Validator. | ||
/// </summary> | ||
internal class IssuerValidatorConstants | ||
{ | ||
public const string Organizations = "organizations"; | ||
public const string Common = "common"; | ||
public const string OidcEndpoint = "/.well-known/openid-configuration"; | ||
public const string FallbackAuthority = "https://login.microsoftonline.com/"; | ||
|
||
/// <summary> | ||
/// Old TenantId claim: "http://schemas.microsoft.com/identity/claims/tenantid". | ||
/// </summary> | ||
public const string TenantId = "http://schemas.microsoft.com/identity/claims/tenantid"; | ||
|
||
/// <summary> | ||
/// New Tenant Id claim: "tid". | ||
/// </summary> | ||
public const string Tid = "tid"; | ||
|
||
/// <summary> | ||
/// Tfp claim: "tfp". | ||
/// </summary> | ||
public const string Tfp = "tfp"; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
|
||
namespace Microsoft.Identity.Web | ||
{ | ||
internal class IssuerValidatorErrorMessage | ||
{ | ||
// Token Validation IDW10300 = "IDW10300:" | ||
public const string IssuerMetadataUrlIsRequired = "IDW10301: Azure AD Issuer metadata address URL is required. "; | ||
public const string NoMetadataDocumentRetrieverProvided = "IDW10302: No metadata document retriever is provided. "; | ||
public const string IssuerDoesNotMatchValidIssuers = "IDW10303: Issuer: '{0}', does not match any of the valid issuers provided for this application. "; | ||
public const string B2CTfpIssuerNotSupported = "IDW10304: Microsoft Identity Web does not support a B2C issuer with 'tfp' in the URI. See https://aka.ms/ms-id-web/b2c-issuer for details. "; | ||
|
||
// Protocol IDW10400 = "IDW10400:" | ||
public const string TenantIdClaimNotPresentInToken = "IDW10401: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. "; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If TVP.ValidIssuer(s) is empty, AadIssuerV1/2 will be populated only once (no refresh). Is this an issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are you suggesting a refresh after a certain amount of time to check if the metadata has changed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. Not sure if the lack of refresh could be the issue (not expecting issuer to change). Code might be broken if the issuer on the metadata changes so that it doesn't contain {tenantId}, and refresh wouldn't help there.