diff --git a/.gitignore b/.gitignore index b58fe4ee..1318374c 100644 --- a/.gitignore +++ b/.gitignore @@ -35,6 +35,7 @@ /2-WebApp-graph-user/2-1-Call-MSGraph/.vs /2-WebApp-graph-user/2-1-Call-MSGraph/bin /2-WebApp-graph-user/2-1-Call-MSGraph/obj +/2-WebApp-graph-user/2-1-Call-MSGraph/TestResults /2-WebApp-graph-user/2-2-TokenCache/.vs /2-WebApp-graph-user/2-2-TokenCache/bin /2-WebApp-graph-user/2-2-TokenCache/obj @@ -110,22 +111,16 @@ /4-WebApp-your-API/Client/bin/Release/netcoreapp2.2 /4-WebApp-your-API/Client/obj/Debug/netcoreapp2.2 /4-WebApp-your-API/Client/obj/Release/netcoreapp2.2 -/Microsoft.Identity.Web.Test/obj/Release/netcoreapp2.2 -/Microsoft.Identity.Web.Test/obj/Debug/netcoreapp2.2 -/5-WebApp-AuthZ/5-2-Groups/bin/Release/netcoreapp2.2 -/5-WebApp-AuthZ/5-1-Roles/bin/Release/netcoreapp2.2 +/Microsoft.Identity.Web.Test/obj +/Microsoft.Identity.Web.Test/bin +/5-WebApp-AuthZ/5-2-Groups/bin +/5-WebApp-AuthZ/5-1-Roles/bin /4-WebApp-your-API/TodoListService/obj /4-WebApp-your-API/TodoListService/bin /4-WebApp-your-API/Client/obj -/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Debug/netcoreapp2.2 +/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin /2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/obj -/2-WebApp-graph-user/2-4-Sovereign-Call-MSGraph/bin/Release/netcoreapp2.2 -/Microsoft.Identity.Web.Test/bin/Release/netcoreapp2.2 /Microsoft.Identity.Web.Test/obj /4-WebApp-your-API/4-2-B2C/.vs /4-WebApp-your-API/4-2-B2C/Client/obj /4-WebApp-your-API/4-2-B2C/TodoListService/obj -/2-WebApp-graph-user/2-3-Multi-Tenant/.vs/WebApp-OpenIDConnect-DotNet -/2-WebApp-graph-user/2-3-Multi-Tenant/bin/Debug/netcoreapp2.2 -/2-WebApp-graph-user/2-3-Multi-Tenant/obj -/Microsoft.Identity.Web.Test/bin/Release/netcoreapp3.0 diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/AspnetCoreWebApp-calls-Microsoft-Graph.sln b/2-WebApp-graph-user/2-1-Call-MSGraph/AspnetCoreWebApp-calls-Microsoft-Graph.sln index 31b29bc0..f7f1f170 100644 --- a/2-WebApp-graph-user/2-1-Call-MSGraph/AspnetCoreWebApp-calls-Microsoft-Graph.sln +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/AspnetCoreWebApp-calls-Microsoft-Graph.sln @@ -7,8 +7,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApp-OpenIDConnect-DotNet EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web", "..\..\Microsoft.Identity.Web\Microsoft.Identity.Web.csproj", "{E0CEF26A-6CE6-4505-851B-6580D5564752}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{CD0BB564-6C1E-4FCE-B9AB-7C637FDEE569}" + ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test", "..\..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.UI", "..\..\Microsoft.Identity.Web.UI\Microsoft.Identity.Web.UI.csproj", "{8CC22202-F66C-4332-A4F0-A2C09EBA08EC}" -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Identity.Web.Test", "..\..\Microsoft.Identity.Web.Test\Microsoft.Identity.Web.Test.csproj", "{0EEC3E2E-69D0-4A7F-98D6-4386330F4965}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -24,14 +30,14 @@ Global {E0CEF26A-6CE6-4505-851B-6580D5564752}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0CEF26A-6CE6-4505-851B-6580D5564752}.Release|Any CPU.Build.0 = Release|Any CPU + {8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CCEAE2A-BDF6-470C-B6DE-7FC81A74DBD7}.Release|Any CPU.Build.0 = Release|Any CPU {8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Debug|Any CPU.Build.0 = Debug|Any CPU {8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CC22202-F66C-4332-A4F0-A2C09EBA08EC}.Release|Any CPU.Build.0 = Release|Any CPU - {0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0EEC3E2E-69D0-4A7F-98D6-4386330F4965}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/2-WebApp-graph-user/2-1-Call-MSGraph/WebApp-OpenIDConnect-DotNet.csproj b/2-WebApp-graph-user/2-1-Call-MSGraph/WebApp-OpenIDConnect-DotNet.csproj index ebd04a6d..2bfb2228 100644 --- a/2-WebApp-graph-user/2-1-Call-MSGraph/WebApp-OpenIDConnect-DotNet.csproj +++ b/2-WebApp-graph-user/2-1-Call-MSGraph/WebApp-OpenIDConnect-DotNet.csproj @@ -18,7 +18,7 @@ - + diff --git a/2-WebApp-graph-user/2-2-TokenCache/README.md b/2-WebApp-graph-user/2-2-TokenCache/README.md index 0ca2aca8..94de5609 100644 --- a/2-WebApp-graph-user/2-2-TokenCache/README.md +++ b/2-WebApp-graph-user/2-2-TokenCache/README.md @@ -175,6 +175,10 @@ Note: if you had used the automation to setup your application mentioned in [Ste Starting from the [previous phase of the tutorial](../../2-WebApp-graph-user/2-1-Call-MSGraph), the code was incrementally updated with the following steps: +### Reference Microsoft.Extensions.Caching.SqlServer + +This sample proposes a distributed SQL token cache. To use it, you'll need to add a reference to the `Microsoft.Extensions.Caching.SqlServer` NuGet package + ### Update the `Startup.cs` file to enable Token caching using Sql database. ```CSharp @@ -205,6 +209,7 @@ The files `MSALAppSqlTokenCacheProvider.cs` and `MSALPerUserSqlTokenCacheProvide ## Next steps - Learn how to enable distributed caches in [token cache serialization](../2.2.%20token%20cache%20serialization) +- Learn more about the [Distributed SQL Server Cache](https://docs.microsoft.com/aspnet/core/performance/caching/distributed#distributed-sql-server-cache) - Learn how the same principle you've just learnt can be used to call: - [several Microsoft APIs](../../3-WebApp-multi-APIs), which will enable you to learn how incremental consent and conditional access is managed in your Web App - 3rd party, or even [your own Web API](../../4-WebApp-your-API), which will enable you to learn about custom scopes diff --git a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs index c818630e..5a879275 100644 --- a/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs +++ b/5-WebApp-AuthZ/5-2-Groups/Services/MicrosoftGraph-Rest/MSGraphService.cs @@ -277,7 +277,7 @@ public async Task GetCurrentUserGroupsAndRolesAsync public async Task> GetMyMemberOfGroupsAsync(string accessToken) { List groups = new List(); - + PrepareAuthenticatedClient(accessToken); // Get groups the current user is a direct member of. IUserMemberOfCollectionWithReferencesPage memberOfGroups = await graphServiceClient.Me.MemberOf.Request().GetAsync(); if (memberOfGroups?.Count > 0) @@ -378,4 +378,4 @@ await Task.Run(() => } } } -} \ No newline at end of file +} diff --git a/Microsoft.Identity.Web.Test/WebApiServiceCollectionExtensionsTests.cs b/Microsoft.Identity.Web.Test/WebApiServiceCollectionExtensionsTests.cs new file mode 100644 index 00000000..fba6c594 --- /dev/null +++ b/Microsoft.Identity.Web.Test/WebApiServiceCollectionExtensionsTests.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System; +using System.Collections.Generic; +using System.Text; +using Xunit; +using System.Linq; + +namespace Microsoft.Identity.Web.Test +{ + public class WebApiServiceCollectionExtensionsTests + { + [Fact] + public void TestAuthority() + { + // Arrange + JwtBearerOptions options = new JwtBearerOptions(); + + // Act and Assert + options.Authority = "https://login.microsoftonline.com/common"; + WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options); + Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority); + + options.Authority = "https://login.microsoftonline.us/organizations"; + WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options); + Assert.Equal("https://login.microsoftonline.us/organizations/v2.0", options.Authority); + + options.Authority = "https://login.microsoftonline.com/common/"; + WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options); + Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority); + + options.Authority = "https://login.microsoftonline.com/common/v2.0"; + WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options); + Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority); + + + options.Authority = "https://login.microsoftonline.com/common/v2.0"; + WebApiServiceCollectionExtensions.EnsureAuthorityIsV2_0(options); + Assert.Equal("https://login.microsoftonline.com/common/v2.0", options.Authority); + + } + + [Fact] + public void TestAudience() + { + JwtBearerOptions options = new JwtBearerOptions(); + + // Act and Assert + options.Audience = "https://localhost"; + WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options); + Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1); + Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "https://localhost"); + + options.Audience = "api://1EE5A092-0DFD-42B6-88E5-C517C0141321"; + WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options); + Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 1); + Assert.True(options.TokenValidationParameters.ValidAudiences.First() == "api://1EE5A092-0DFD-42B6-88E5-C517C0141321"); + + options.Audience = "1EE5A092-0DFD-42B6-88E5-C517C0141321"; + WebApiServiceCollectionExtensions.EnsureValidAudiencesContainsApiGuidIfGuidProvided(options); + Assert.True(options.TokenValidationParameters.ValidAudiences.Count() == 2); + Assert.Contains("api://1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences); + Assert.Contains("1EE5A092-0DFD-42B6-88E5-C517C0141321", options.TokenValidationParameters.ValidAudiences); + + } + } +} diff --git a/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs b/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs index 32f8990f..a022fd59 100644 --- a/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs +++ b/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs @@ -47,6 +47,7 @@ public override void OnException(ExceptionContext context) { string[] incrementalConsentScopes = new string[] { }; MsalUiRequiredException msalUiRequiredException = context.Exception as MsalUiRequiredException; + if (msalUiRequiredException == null) { msalUiRequiredException = context.Exception?.InnerException as MsalUiRequiredException; @@ -56,6 +57,9 @@ public override void OnException(ExceptionContext context) { if (CanBeSolvedByReSignInOfUser(msalUiRequiredException)) { + // Do not re-use the attribute param Scopes. For more info: https://github.com/Azure-Samples/active-directory-aspnetcore-webapp-openidconnect-v2/issues/273 + string[] scopes = null; + // the users cannot provide both scopes and ScopeKeySection at the same time if (!string.IsNullOrWhiteSpace(ScopeKeySection) && Scopes != null && Scopes.Length > 0) { @@ -73,15 +77,17 @@ public override void OnException(ExceptionContext context) throw new InvalidOperationException($"The {nameof(ScopeKeySection)} is provided but the IConfiguration instance is not present in the services collection"); } - incrementalConsentScopes = new string[] { configuration.GetValue(ScopeKeySection) }; - } - - if (Scopes != null && Scopes.Length > 0 && incrementalConsentScopes != null && incrementalConsentScopes.Length > 0) - { - throw new InvalidOperationException("no scopes provided here..."); + scopes = new string[] { configuration.GetValue(ScopeKeySection) }; + + if (Scopes != null && Scopes.Length > 0 && scopes != null && scopes.Length > 0) + { + throw new InvalidOperationException("no scopes provided in scopes..."); + } } + else + scopes = Scopes; - var properties = BuildAuthenticationPropertiesForIncrementalConsent(incrementalConsentScopes, msalUiRequiredException, context.HttpContext); + var properties = BuildAuthenticationPropertiesForIncrementalConsent(scopes, msalUiRequiredException, context.HttpContext); context.Result = new ChallengeResult(properties); } } diff --git a/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj b/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj index 7f4c898b..b632d941 100644 --- a/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj +++ b/Microsoft.Identity.Web/Microsoft.Identity.Web.csproj @@ -34,7 +34,7 @@ - + @@ -55,7 +55,8 @@ - - + + + diff --git a/Microsoft.Identity.Web/TokenAcquisition.cs b/Microsoft.Identity.Web/TokenAcquisition.cs index a940d3d8..52109c06 100644 --- a/Microsoft.Identity.Web/TokenAcquisition.cs +++ b/Microsoft.Identity.Web/TokenAcquisition.cs @@ -310,22 +310,24 @@ private IConfidentialClientApplication BuildConfidentialClientApplication() request.PathBase, microsoftIdentityOptions.CallbackPath.Value ?? string.Empty); - string authority = string.Empty; + if (!applicationOptions.Instance.EndsWith("/")) + applicationOptions.Instance += "/"; + + string authority ; IConfidentialClientApplication app = null; if (microsoftIdentityOptions.IsB2C) { - authority = $"{applicationOptions.Instance.TrimEnd('/')}/tfp/{microsoftIdentityOptions.Domain}/{microsoftIdentityOptions.DefaultUserFlow}"; + authority = $"{applicationOptions.Instance}tfp/{microsoftIdentityOptions.Domain}/{microsoftIdentityOptions.DefaultUserFlow}"; app = ConfidentialClientApplicationBuilder .CreateWithApplicationOptions(applicationOptions) .WithRedirectUri(currentUri) .WithB2CAuthority(authority) .Build(); } - else { - authority = $"{applicationOptions.Instance.TrimEnd('/')}/{applicationOptions.TenantId}/"; + authority = $"{applicationOptions.Instance}{applicationOptions.TenantId}/"; app = ConfidentialClientApplicationBuilder .CreateWithApplicationOptions(applicationOptions) .WithRedirectUri(currentUri) diff --git a/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs b/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs index 1f3964a5..4f8c8a6c 100644 --- a/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs +++ b/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs @@ -166,8 +166,12 @@ public static AuthenticationBuilder AddProtectedWebApi( if (string.IsNullOrWhiteSpace(options.Authority)) options.Authority = AuthorityHelpers.BuildAuthority(microsoftIdentityOptions); - if (!AuthorityHelpers.IsV2Authority(options.Authority)) - options.Authority += "/v2.0"; + // This is an Microsoft identity platform Web API + EnsureAuthorityIsV2_0(options); + + // The valid audience could be given as Client Id or as Uri. + // If it does not start with 'api://', this variant is added to the list of valid audiences. + EnsureValidAudiencesContainsApiGuidIfGuidProvided(options); options.TokenValidationParameters.ValidAudiences = GetValidAudiences(options, microsoftIdentityOptions); @@ -244,26 +248,36 @@ public static IServiceCollection AddProtectedWebApiCallsProtectedWebApi( return services; } - internal static List GetValidAudiences( - JwtBearerOptions options, - MicrosoftIdentityOptions msIdentityOptions) + /// + /// Ensures that the authority is a v2.0 authority + /// + /// Jwt bearer options read from the config file + /// or set by the developper, for which we want to ensure the authority + /// is a v2.0 authority + internal static void EnsureAuthorityIsV2_0(JwtBearerOptions options) { - var validAudiences = new List(); + var authority = options.Authority.Trim().TrimEnd('/'); + if (!authority.EndsWith("v2.0")) + authority += "/v2.0"; + options.Authority = authority; + } - // The valid audiences are both the Client ID (options.Audience) and api://{ClientID} - // If the developer doesn't set the Audience on JwtBearerOptions, use ClientId from MicrosoftIdentityOptions - if (!string.IsNullOrEmpty(options.Audience)) - { - validAudiences.Add(options.Audience); + + /// + /// Ensure that if the audience is a GUID, api://{audience} is also added + /// as a valid audience (this is the default App ID URL in the app registration + /// portal) + /// + /// Jwt bearer options for which to ensure that + /// api://GUID is a valid audience + internal static void EnsureValidAudiencesContainsApiGuidIfGuidProvided(JwtBearerOptions options) + { + var validAudiences = new List { options.Audience }; + if (!options.Audience.StartsWith("api://", StringComparison.OrdinalIgnoreCase) + && Guid.TryParse(options.Audience, out _)) validAudiences.Add($"api://{options.Audience}"); - } - else - { - validAudiences.Add(msIdentityOptions.ClientId); - validAudiences.Add($"api://{msIdentityOptions.ClientId}"); - } - return validAudiences; + options.TokenValidationParameters.ValidAudiences = validAudiences; } } -} \ No newline at end of file +}