diff --git a/4-WebApp-your-API/4-2-B2C/Client/Views/Shared/_LoginPartial.cshtml b/4-WebApp-your-API/4-2-B2C/Client/Views/Shared/_LoginPartial.cshtml
index 8ee2ae41..82a0ca9f 100644
--- a/4-WebApp-your-API/4-2-B2C/Client/Views/Shared/_LoginPartial.cshtml
+++ b/4-WebApp-your-API/4-2-B2C/Client/Views/Shared/_LoginPartial.cshtml
@@ -3,6 +3,11 @@
{
- Hello @User.GetDisplayName()!
+ -
+
+
- Sign out
}
diff --git a/4-WebApp-your-API/4-2-B2C/Client/appsettings.json b/4-WebApp-your-API/4-2-B2C/Client/appsettings.json
index ce742ed1..835f15d2 100644
--- a/4-WebApp-your-API/4-2-B2C/Client/appsettings.json
+++ b/4-WebApp-your-API/4-2-B2C/Client/appsettings.json
@@ -10,17 +10,6 @@
"ClientSecret": "X330F3#92!z614M4"
//"CallbackPath": "/signin/B2C_1_sign_up_in" // defaults to /signin-oidc
},
- "AzureAd": {
- "Instance": "https://login.microsoftonline.com/",
- "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
- "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
- "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]",
- "CallbackPath": "/signin-oidc",
- "SignedOutCallbackPath ": "/signout-callback-oidc",
-
- // To call an API
- "ClientSecret": "[Copy the client secret added to the app from the Azure portal]"
- },
"TodoList": {
/*
TodoListScope is the scope of the Web API you want to call. This can be: "api://8f085429-c424-45c4-beb3-75f6f0a7924f/user_impersonation",
diff --git a/4-WebApp-your-API/4-2-B2C/TodoListService/AuthorizationPolicies/ScopesRequirement.cs b/4-WebApp-your-API/4-2-B2C/TodoListService/AuthorizationPolicies/ScopesRequirement.cs
new file mode 100644
index 00000000..8b213be3
--- /dev/null
+++ b/4-WebApp-your-API/4-2-B2C/TodoListService/AuthorizationPolicies/ScopesRequirement.cs
@@ -0,0 +1,50 @@
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
+using Microsoft.Identity.Web;
+using System;
+using System.Linq;
+using System.Security.Claims;
+using System.Threading.Tasks;
+
+namespace TodoListService.AuthorizationPolicies
+{
+ ///
+ /// Requirement used in authorization policies, to check if the scope claim has at least one of the requirement values.
+ /// Since the class also extends AuthorizationHandler, its dependency injection is done out of the box.
+ ///
+ public class ScopesRequirement : AuthorizationHandler, IAuthorizationRequirement
+ {
+ string[] _acceptedScopes;
+
+ public ScopesRequirement(params string[] acceptedScopes)
+ {
+ _acceptedScopes = acceptedScopes;
+ }
+
+ ///
+ /// AuthorizationHandler that will check if the scope claim has at least one of the requirement values
+ ///
+ protected override Task HandleRequirementAsync(AuthorizationHandlerContext context,
+ ScopesRequirement requirement)
+ {
+ // If there are no scopes, do not process
+ if (!context.User.Claims.Any(x => x.Type == ClaimConstants.Scope)
+ && !context.User.Claims.Any(y => y.Type == ClaimConstants.Scp))
+ {
+ return Task.CompletedTask;
+ }
+
+ Claim scopeClaim = context?.User?.FindFirst(ClaimConstants.Scp);
+
+ if (scopeClaim == null)
+ scopeClaim = context?.User?.FindFirst(ClaimConstants.Scope);
+
+ if (scopeClaim != null && scopeClaim.Value.Split(' ').Intersect(requirement._acceptedScopes).Any())
+ {
+ context.Succeed(requirement);
+ }
+
+ return Task.CompletedTask;
+ }
+ }
+}
diff --git a/4-WebApp-your-API/4-2-B2C/TodoListService/Controllers/TodoListController.cs b/4-WebApp-your-API/4-2-B2C/TodoListService/Controllers/TodoListController.cs
index a271cd64..ba25f7f2 100644
--- a/4-WebApp-your-API/4-2-B2C/TodoListService/Controllers/TodoListController.cs
+++ b/4-WebApp-your-API/4-2-B2C/TodoListService/Controllers/TodoListController.cs
@@ -54,6 +54,7 @@ public TodoListController(IHttpContextAccessor contextAccessor)
// GET: api/values
[HttpGet]
+ [Authorize(Policy = "ReadScope")]
public IEnumerable Get()
{
string owner = User.Identity.Name;
@@ -62,6 +63,7 @@ public IEnumerable Get()
// GET: api/values
[HttpGet("{id}", Name = "Get")]
+ [Authorize(Policy = "ReadScope")]
public Todo Get(int id)
{
return TodoStore.Values.FirstOrDefault(t => t.Id == id);
diff --git a/4-WebApp-your-API/4-2-B2C/TodoListService/Startup.cs b/4-WebApp-your-API/4-2-B2C/TodoListService/Startup.cs
index 25b77068..bee405b3 100644
--- a/4-WebApp-your-API/4-2-B2C/TodoListService/Startup.cs
+++ b/4-WebApp-your-API/4-2-B2C/TodoListService/Startup.cs
@@ -8,6 +8,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Identity.Web;
using Microsoft.AspNetCore.Authentication.JwtBearer;
+using Microsoft.AspNetCore.Authorization;
+using TodoListService.AuthorizationPolicies;
+using Microsoft.AspNetCore.Authorization.Infrastructure;
namespace TodoListService
{
@@ -31,9 +34,20 @@ public void ConfigureServices(IServiceCollection services)
// Adds Microsoft Identity platform (AAD v2.0) support to protect this Api
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddProtectedWebApi("AzureAdB2C", Configuration, options => Configuration.Bind("AzureAdB2C", options));
+ .AddProtectedWebApi("AzureAdB2C", Configuration, options =>
+ {
+ Configuration.Bind("AzureAdB2C", options);
+
+ options.TokenValidationParameters.NameClaimType = "name";
+ });
services.AddControllers();
+ services.AddAuthorization(options =>
+ {
+ // Create policy to check for the scope 'read'
+ options.AddPolicy("ReadScope",
+ policy => policy.Requirements.Add(new ScopesRequirement("read")));
+ });
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
diff --git a/4-WebApp-your-API/4-2-B2C/TodoListService/appsettings.json b/4-WebApp-your-API/4-2-B2C/TodoListService/appsettings.json
index 9346d092..b37df85a 100644
--- a/4-WebApp-your-API/4-2-B2C/TodoListService/appsettings.json
+++ b/4-WebApp-your-API/4-2-B2C/TodoListService/appsettings.json
@@ -9,13 +9,6 @@
"EditProfilePolicyId": "b2c_1_edit_profile" // Optional profile editing policy
//"CallbackPath": "/signin/B2C_1_sign_up_in" // defaults to /signin-oidc
},
- "AzureAd": {
- "Instance": "https://login.microsoftonline.com/",
- "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]",
- "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]",
- "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]"
-
- },
"Kestrel": {
"Endpoints": {
"Http": {
diff --git a/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs b/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs
index a022fd59..aaf95599 100644
--- a/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs
+++ b/Microsoft.Identity.Web/AuthorizeForScopesAttribute.cs
@@ -45,6 +45,7 @@ public class AuthorizeForScopesAttribute : ExceptionFilterAttribute
/// Context provided by ASP.NET Core
public override void OnException(ExceptionContext context)
{
+ // 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[] incrementalConsentScopes = new string[] { };
MsalUiRequiredException msalUiRequiredException = context.Exception as MsalUiRequiredException;
@@ -57,9 +58,6 @@ 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)
{
@@ -77,17 +75,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");
}
- scopes = new string[] { configuration.GetValue(ScopeKeySection) };
+ incrementalConsentScopes = new string[] { configuration.GetValue(ScopeKeySection) };
- if (Scopes != null && Scopes.Length > 0 && scopes != null && scopes.Length > 0)
+ if (Scopes != null && Scopes.Length > 0 && incrementalConsentScopes != null && incrementalConsentScopes.Length > 0)
{
throw new InvalidOperationException("no scopes provided in scopes...");
}
}
else
- scopes = Scopes;
+ incrementalConsentScopes = Scopes;
- var properties = BuildAuthenticationPropertiesForIncrementalConsent(scopes, msalUiRequiredException, context.HttpContext);
+ var properties = BuildAuthenticationPropertiesForIncrementalConsent(incrementalConsentScopes, msalUiRequiredException, context.HttpContext);
context.Result = new ChallengeResult(properties);
}
}
diff --git a/Microsoft.Identity.Web/ClaimConstants.cs b/Microsoft.Identity.Web/ClaimConstants.cs
index 390f1918..b0e562dc 100644
--- a/Microsoft.Identity.Web/ClaimConstants.cs
+++ b/Microsoft.Identity.Web/ClaimConstants.cs
@@ -21,6 +21,7 @@ public static class ClaimConstants
// Newer scope claim
public const string Scp = "scp";
public const string Roles = "roles";
+ public const string Role = "http://schemas.microsoft.com/ws/2008/06/identity/claims/role";
public const string Sub = "sub";
// Policy claims
diff --git a/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs b/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs
index 2fea242b..d9bc5ed3 100644
--- a/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs
+++ b/Microsoft.Identity.Web/WebApiServiceCollectionExtensions.cs
@@ -198,7 +198,8 @@ public static AuthenticationBuilder AddProtectedWebApi(
// This check is required to ensure that the Web API only accepts tokens from tenants where it has been consented and provisioned.
if (!context.Principal.Claims.Any(x => x.Type == ClaimConstants.Scope)
&& !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Scp)
- && !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles))
+ && !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Roles)
+ && !context.Principal.Claims.Any(y => y.Type == ClaimConstants.Role))
{
throw new UnauthorizedAccessException("Neither scope or roles claim was found in the bearer token.");
}
diff --git a/Microsoft.Identity.Web/WebAppServiceCollectionExtensions.cs b/Microsoft.Identity.Web/WebAppServiceCollectionExtensions.cs
index 929aec76..74069341 100644
--- a/Microsoft.Identity.Web/WebAppServiceCollectionExtensions.cs
+++ b/Microsoft.Identity.Web/WebAppServiceCollectionExtensions.cs
@@ -308,50 +308,5 @@ public static AuthenticationBuilder AddSignIn(
return builder;
}
-
-
- // Method taken from https://devblogs.microsoft.com/aspnet/upcoming-samesite-cookie-changes-in-asp-net-and-asp-net-core/
- public static bool DisallowsSameSiteNone(string userAgent)
- {
- if (string.IsNullOrEmpty(userAgent))
- {
- return false;
- }
-
- // Cover all iOS based browsers here. This includes:
- // - Safari on iOS 12 for iPhone, iPod Touch, iPad
- // - WkWebview on iOS 12 for iPhone, iPod Touch, iPad
- // - Chrome on iOS 12 for iPhone, iPod Touch, iPad
- // All of which are broken by SameSite=None, because they use the iOS networking
- // stack.
- if (userAgent.Contains("CPU iPhone OS 12") ||
- userAgent.Contains("iPad; CPU OS 12"))
- {
- return true;
- }
-
- // Cover Mac OS X based browsers that use the Mac OS networking stack.
- // This includes:
- // - Safari on Mac OS X.
- // This does not include:
- // - Chrome on Mac OS X
- // Because they do not use the Mac OS networking stack.
- if (userAgent.Contains("Macintosh; Intel Mac OS X 10_14") &&
- userAgent.Contains("Version/") && userAgent.Contains("Safari"))
- {
- return true;
- }
-
- // Cover Chrome 50-69, because some versions are broken by SameSite=None,
- // and none in this range require it.
- // Note: this covers some pre-Chromium Edge versions,
- // but pre-Chromium Edge does not require SameSite=None.
- if (userAgent.Contains("Chrome/5") || userAgent.Contains("Chrome/6"))
- {
- return true;
- }
-
- return false;
- }
}
}