How to redirect after Azure AD authentication to different controller action in ASP Net Core MVC

家住魔仙堡 提交于 2019-12-02 01:30:19

I have found a way to make it work by using a redirect as follows...

Inside Startup

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Account}/{action=SignIn}/{id?}");
});

Inside AccountController

// GET: /Account/CheckSignIn
[HttpGet]
[Authorize]
public IActionResult CheckSignIn()
{
    //add code here to check if AzureAD identity exists in user table in local database
    //if not then insert new user record into local user table

    return RedirectToAction(nameof(HomeController.Index), "Home");
}

//
// GET: /Account/SignIn
[HttpGet]
public IActionResult SignIn()
{
    return Challenge(
        new AuthenticationProperties { RedirectUri = "/Account/CheckSignIn" }, OpenIdConnectDefaults.AuthenticationScheme);
}

Inside AzureAdServiceCollectionExtensions (.net core 2.0)

private static Task RedirectToIdentityProvider(RedirectContext context)
{
    if (context.Request.Path != new PathString("/"))
    {
        context.Properties.RedirectUri = new PathString("/Account/CheckSignIn");
    }
    return Task.FromResult(0);
}
Nan Yu

The default behavior is: user will be redirected to the original page. For example, user is not authenticated and access Index page, after authenticated, he will be redirected to Index page; user is not authenticated and access Contact page, after authenticated, he will be redirected to Contact page.

As a workaround, you can modify the default website route to redirect user to specific controller/action:

app.UseMvc(routes =>
{
    routes.MapRoute(
        name: "default",
        template: "{controller=Account}/{action=CheckSignIn}/{id?}"
    );
});

After your custom logic, you could redirect user to your truly default page(Home/Index).

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<string>($"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<CookieAuthenticationOptions>(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<HasUserIdAuthorizationRequirement>
    {
        // 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<IAuthorizationHandler, HasUserIdAuthorizationHandler>();

        // My custom service to access user data from the database:
        services.AddScoped<IUserService, UserService>();

And finally, in _LoginPartial.cshtml change the SignIn action from:

<a class="nav-link text-dark" asp-area="AzureADB2C" asp-controller="Account" asp-action="SignIn">Sign in</a>

To:

<a class="nav-link text-dark" asp-controller="Account" asp-action="LogOn">Sign in</a>

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.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!