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

updates to id web to support authentication handlers than JwtBearer #1493

Closed
wants to merge 3 commits into from
Closed
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
13 changes: 13 additions & 0 deletions Microsoft.Identity.Web.sln
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Toke
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Certificate", "src\Microsoft.Identity.Web.Certificate\Microsoft.Identity.Web.Certificate.csproj", "{1E0B96CD-FDBF-482C-996A-775F691D984E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.IssuerValidator", "src\Microsoft.Identity.Web.IssuerValidator\Microsoft.Identity.Web.IssuerValidator.csproj", "{5F3F1DD3-5357-43EE-8ECE-9EED72ACDDFC}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.IssuerValidator.Test", "tests\Microsoft.Identity.Web.IssuerValidator.Test\Microsoft.Identity.Web.IssuerValidator.Test.csproj", "{7E7E80AE-2416-40A7-858E-507C4A05A57B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -300,6 +304,14 @@ Global
{1E0B96CD-FDBF-482C-996A-775F691D984E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1E0B96CD-FDBF-482C-996A-775F691D984E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1E0B96CD-FDBF-482C-996A-775F691D984E}.Release|Any CPU.Build.0 = Release|Any CPU
{5F3F1DD3-5357-43EE-8ECE-9EED72ACDDFC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5F3F1DD3-5357-43EE-8ECE-9EED72ACDDFC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5F3F1DD3-5357-43EE-8ECE-9EED72ACDDFC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5F3F1DD3-5357-43EE-8ECE-9EED72ACDDFC}.Release|Any CPU.Build.0 = Release|Any CPU
{7E7E80AE-2416-40A7-858E-507C4A05A57B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7E7E80AE-2416-40A7-858E-507C4A05A57B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7E7E80AE-2416-40A7-858E-507C4A05A57B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7E7E80AE-2416-40A7-858E-507C4A05A57B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -354,6 +366,7 @@ Global
{D3A42D78-8A23-44EB-9A1F-63CF58B56AAC} = {9A51E6E0-A2B0-4D4D-9209-E73B8AD2B9C6}
{15B7F1B3-A438-4A42-BFFA-E3382543C037} = {D3A42D78-8A23-44EB-9A1F-63CF58B56AAC}
{5B9AD363-6A27-4AF2-939E-B20E7C941ADF} = {20CDB9B2-D237-413A-8E76-A973597748B0}
{7E7E80AE-2416-40A7-858E-507C4A05A57B} = {79310504-1334-4F14-93C4-1240913224BA}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {F4FA8C4C-3251-41CC-939B-7892F5798549}
Expand Down
4 changes: 2 additions & 2 deletions build/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.MicrosoftGraphBeta')`
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\ProjectTemplates')`
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.TokenCache')`
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.Certificate')
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Certificate')`
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.Certificate')`
- [template-pack-and-sign-nuget.yaml](template-pack-and-sign-nuget.yaml) `('$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.IssuerValidator')`
- 'Copy Files from `$(Build.SourcesDirectory)` to: `$(Build.ArtifactStagingDirectory)\packages'`
- Sign Packages `'('$(Build.ArtifactStagingDirectory)\packages')`
- [template-publish-packages-and-symbols.yaml](template-publish-packages-and-symbols.yaml)
Expand Down
11 changes: 9 additions & 2 deletions build/template-pack-and-sign-all-nugets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,27 @@ steps:
ProjectRootPath: '$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.MicrosoftGraphBeta'
AssemblyName: 'Microsoft.Identity.Web.MicrosoftGraphBeta*'

# Pack and sign Microsoft.Identity.TokenCache
# Pack and sign Microsoft.Identity.Web.TokenCache
- template: template-pack-and-sign-nuget.yaml
parameters:
BuildConfiguration: ${{ parameters.BuildConfiguration }}
ProjectRootPath: '$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.TokenCache'
AssemblyName: 'Microsoft.Identity.Web.TokenCache*'

# Pack and sign Microsoft.Identity.Certificate
# Pack and sign Microsoft.Identity.Web.Certificate
- template: template-pack-and-sign-nuget.yaml
parameters:
BuildConfiguration: ${{ parameters.BuildConfiguration }}
ProjectRootPath: '$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.Certificate'
AssemblyName: 'Microsoft.Identity.Web.Certificate*'

# Pack and sign Microsoft.Identity.Web.IssuerValidator
- template: template-pack-and-sign-nuget.yaml
parameters:
BuildConfiguration: ${{ parameters.BuildConfiguration }}
ProjectRootPath: '$(Build.SourcesDirectory)\src\Microsoft.Identity.Web.IssuerValidator'
AssemblyName: 'Microsoft.Identity.Web.IssuerValidator*'

# Pack and sign Microsoft.Identity.Web.ProjectTemplates
- template: template-pack-and-sign-nuget.yaml
parameters:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
namespace Microsoft.Identity.Web
{
/// <summary>
/// General constants for Microsoft Identity Certificates.
/// General constants for Microsoft Identity Web Certificates.
/// </summary>
internal static class CertificateConstants
{
Expand Down

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
Expand Up @@ -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;
Expand All @@ -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; }
Copy link
Member

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?

Copy link
Collaborator Author

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?

Copy link
Member

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.

internal string? AadIssuerV1 { get; set; }
internal string? AadIssuerV2 { get; set; }
internal string AadAuthority { get; set; }
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -132,36 +129,46 @@ public string Validate(
throw new SecurityTokenInvalidIssuerException(
string.Format(
CultureInfo.InvariantCulture,
IDWebErrorMessage.IssuerDoesNotMatchValidIssuers,
IssuerValidatorErrorMessage.IssuerDoesNotMatchValidIssuers,
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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))
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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());
}
}
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
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
Expand Up @@ -27,12 +27,12 @@ public async Task<IssuerMetadata> GetConfigurationAsync(string address, IDocumen
{
if (string.IsNullOrEmpty(address))
{
throw new ArgumentNullException(nameof(address), IDWebErrorMessage.IssuerMetadataUrlIsRequired);
throw new ArgumentNullException(nameof(address), IssuerValidatorErrorMessage.IssuerMetadataUrlIsRequired);
}

if (retriever == null)
{
throw new ArgumentNullException(nameof(retriever), IDWebErrorMessage.NoMetadataDocumentRetrieverProvided);
throw new ArgumentNullException(nameof(retriever), IssuerValidatorErrorMessage.NoMetadataDocumentRetrieverProvided);
}

var options = new JsonSerializerOptions
Expand Down
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. ";
}
}
Loading