I have setup my ASP Net Core 2.0 project to authenticate with Azure AD (using the standard Azure AD Identity Authentication template in VS2017 which uses OIDC). Everything i
I want to check if the user exists in my local database, not only when Sign in is selected, but also when any other link to my website is clicked which requires authentication.
After a lot of trial and error I found a solution. Not sure if it is the best solution, but it works.
Basically I use the Authorize attribute with a policy [Authorize(Policy = "HasUserId")]
as described in Claims-based authorization in ASP.NET Core.
Now when the policy is not met, you can reroute to the register action.
A – very simplified – version of the AccountController would look like this (I use a LogOn action instead of SignIn to prevent conflicts with the AzureADB2C AccountController):
public class AccountController : Controller
{
public IActionResult AccessDenied([FromQuery] string returnUrl)
{
if (User.Identity.IsAuthenticated)
return RedirectToAction(nameof(Register), new { returnUrl });
return new ActionResult($"Access denied: {returnUrl}").Result;
}
public IActionResult LogOn()
{
// TODO: set redirectUrl to the view you want to show when a registerd user is logged on.
var redirectUrl = Url.Action("Test");
return Challenge(
new AuthenticationProperties { RedirectUri = redirectUrl },
AzureADB2CDefaults.AuthenticationScheme);
}
// User must be authorized to register, but does not have to meet the policy:
[Authorize]
public string Register([FromQuery] string returnUrl)
{
// TODO Register user in local database and after successful registration redirect to returnUrl.
return $"This is the Account:Register action method. returnUrl={returnUrl}";
}
// Example of how to use the Authorize attribute with a policy.
// This action will only be executed whe the user is logged on AND registered.
[Authorize(Policy = "HasUserId")]
public string Test()
{
return "This is the Account:Test action method...";
}
}
In Startup.cs, in the ConfigureServices method, set the AccessDeniedPath:
services.Configure(AzureADB2CDefaults.CookieScheme,
options => options.AccessDeniedPath = new PathString("/Account/AccessDenied/"));
A quick-and-dirty way to implement the HasUserId policy is to add the UserId from your local database as a claim in the OnSigningIn event of the CookieAuthenticationOptions and then use RequireClaim to check for the UserId claim. But because I need my data context (with a scoped lifetime) I used an AuthorizationRequirement with an AuthorizationHandler (see Authorization Requirements):
The AuthorizationRequirement is in this case just an empty marker class:
using Microsoft.AspNetCore.Authorization;
namespace YourAppName.Authorization
{
public class HasUserIdAuthorizationRequirement : IAuthorizationRequirement
{
}
}
Implementation of the AuthorizationHandler:
public class HasUserIdAuthorizationHandler : AuthorizationHandler
{
// Warning: To make sure the Azure objectidentifier is present,
// make sure to select in your Sign-up or sign-in policy (user flow)
// in the Return claims section: User's Object ID.
private const string ClaimTypeAzureObjectId = "http://schemas.microsoft.com/identity/claims/objectidentifier";
private readonly IUserService _userService;
public HasUserIdAuthorizationHandler(IUserService userService)
{
_userService = userService;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, HasUserIdAuthorizationRequirement requirement)
{
// Load User Id from database:
var azureObjectId = context.User?.FindFirst(ClaimTypeAzureObjectId)?.Value;
var userId = await _userService.GetUserIdForAzureUser(azureObjectId);
if (userId == 0)
return;
context.Succeed(requirement);
}
}
_userService.GetUserIdForAzureUser
searches for an existing UserId in the database, connected to the azureObjectId and returns 0 when not found or when azureObjectId is null.
In Startup.cs, in the ConfigureServices method, add the Authorization policy and the AuthorizationHandler:
services.AddAuthorization(options => options.AddPolicy("HasUserId",
policy => policy.Requirements.Add(new HasUserIdAuthorizationRequirement())));
// AddScoped used for the HasUserIdAuthorizationHandler, because it uses the
// data context with a scoped lifetime.
services.AddScoped();
// My custom service to access user data from the database:
services.AddScoped();
And finally, in _LoginPartial.cshtml change the SignIn action from:
Sign in
To:
Sign in
Now, when the user is not logged on and clicks Sign in, or any link to an action or controller decorated with [Authorize(Policy="HasUserId")]
, he will first be rerouted to the AD B2C logon page. Then, after logon, when the user is already registered, he will be rerouted to the selected link. When not registered, he will be rerouted to the Account/Register action.
Remark: If using policies does not fit well for your solution, take a look at https://stackoverflow.com/a/41348219.