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
+}