4

I created a new .net core 3.1.1 web application with the Razor Pages framework. When creating the app I set up the default Authentication as AzureAd. When I run the application the authentication works just fine. The generated appsettings file looks like this:

{
  "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "myDomain",
    "TenantId": "myTenantId",
    "ClientId": "myClientId",
    "CallbackPath": "/signin-oidc"
  },
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}

I created a new controller in my app which looks very simple, just like:

namespace WebApplication1.Controllers
{
    public class AccountController : Controller
    {
        [HttpGet]
        public void SignIn()
        {
           //here comes the logic which checks in what role is the logged User
           //the role management stuff will be implemented in the app
        }
    }
}

This is how my Startup.cs looks like:

public class Startup
{
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    public IConfiguration Configuration { get; }

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
            .AddAzureAD(options => Configuration.Bind("AzureAd", options));
        services.AddMvc(options =>
        {
            options.EnableEndpointRouting = false;
        });
        services.AddRazorPages().AddMvcOptions(options =>{});
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Error");
            // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
            app.UseHsts();
        }

        app.UseHttpsRedirection();
        app.UseStaticFiles();

        app.UseRouting();

        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
            endpoints.MapControllers();
        });

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

I'd would like to be able to change the AzureAd/CallbackPath to something different than "/signin-oidc" eg. I would like to change it to Account/SignIn. Then I'd like to catch the callback call from azure and based on the logged user email address I'd like to modify the token to add some system roles and make a redirect to the appropriate dashboard page based on the user role. There can be a different dashboard for admin and a client.

So I tried to change the "CallbackPath": "/Account/SignIn" and I also updated RedirectURI in Azure: RedirectURI in Azure

Then I run the app once again, set a breakpoint in void SignIn(), I signed in once again, and instead of hitting the /Account/SignIn I was just redirected to the main page, the https://localhost:44321. I also tried to manually run the https://localhost:44321/Account/SignIn in the browser and I saw the following error message:

An unhandled exception occurred while processing the request.
Exception: OpenIdConnectAuthenticationHandler: message.State is null or empty.

enter image description here

I tried to check if there is something in the documentation but I didn't find anything useful. Any ideas about what should I do to make it work? Cheers

EDIT:

I also use Microsoft.AspNetCore.Authentication.AzureAD.UI framework.

GoldenAge
  • 2,918
  • 5
  • 25
  • 63

2 Answers2

12

The CallbackPath is the path where server will redirect during authentication. It's automatically handled by the OIDC middleware itself, that means we can't control the logic by creating a new controller/action and set CallbackPath to it . Below is the general process :

During authentication , the whole process is controlled by OpenID Connect middleware , after user validate credential in Azure's login page ,Azure Ad will redirect user back to your application's redirect url which is set in OIDC's configuration , so that you can get the authorization code(if using code flow) and complete the authentication process . After authentication , user will then be redirected to the redirect url .

based on the logged user email address I'd like to modify the token to add some system roles and make a redirect to the appropriate dashboard page based on the user role. There can be a different dashboard for admin and a client.

The first thing is you can't modify the token and you don't need to modify that .

You can use notification events in OIDC OWIN Middlerware which invokes to enable developer control over the authentication process . OnTokenValidated offers you the chance to modify the ClaimsIdentity obtained from the incoming token , you can query user's role based on user's id from local database and add to user's claims :

 services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
            .AddAzureAD(options => Configuration.Bind("AzureAd", options));


services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
    options.Events = new OpenIdConnectEvents
    {
        OnTokenValidated = ctx =>
        {
            //query the database to get the role

            // add claims
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Role, "Admin")
            };
            var appIdentity = new ClaimsIdentity(claims);

            ctx.Principal.AddIdentity(appIdentity);

            return Task.CompletedTask;
        },
    };
});

Then in controller , you can get the claim like :

var role = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value;

Then you can filter the actions based on specific claim .

If you want to redirect user to specific route/page after authentication , put the url to AuthenticationProperties :

if (!User.Identity.IsAuthenticated)
{
    return Challenge(new AuthenticationProperties() { RedirectUri = "/home/redirectOnRole" } , AzureADDefaults.AuthenticationScheme);
}  

And in that path , you can redirect user based on user's role .

Nan Yu
  • 26,101
  • 9
  • 68
  • 148
  • Where should I put the `if (!User.Identity.IsAuthenticated) { return Challenge(new AuthenticationProperties() { RedirectUri = "/home/redirectOnRole" } , AzureADDefaults.AuthenticationScheme); } ` from your answer? The User comes from Microsoft.AspNetCore.Mvc.Razor so I'm not sure If I can do this in the middleware? – GoldenAge Apr 27 '20 at 11:50
  • I also use the `Microsoft.AspNetCore.Authentication.AzureAD.UI`, so the redirect to the AzureAD login page happens thru this framework – GoldenAge Apr 27 '20 at 12:06
  • I've just tried adding `await context.HttpContext.ChallengeAsync(AzureADDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/contact" });` to the `OnTokenValidated` but redirect didn't work after I re-logged the user:( – GoldenAge Apr 27 '20 at 12:26
  • No ,that codes should in one action which trigger the authentication . – Nan Yu Apr 28 '20 at 01:30
  • my code sample also use the `Microsoft.AspNetCore.Authentication.AzureAD.UI` ;library . Your have two choice : 1. after authentication , user will be redirect to index page, you can then redirect user based on user's role in index action , i have provide codes sample for how to add role claim above . – Nan Yu Apr 28 '20 at 01:35
  • 2. in login method of your razor page , use `return Challenge(new AuthenticationProperties() { RedirectUri = "/home/redirectOnRole" } , AzureADDefaults.AuthenticationScheme);` to manually set a url , user will be redirect to that url after authentication , in that url you can redirect to another actions based on user role . – Nan Yu Apr 28 '20 at 01:36
  • Because I'm using the AzureAd.UI library I don't have any own SignIn method. When I created the app the partial view called `_LoginPartial` was automatically added t to `Pages/Shared` and it contains a link which looks like: `Sign in` this means that the SignIn action is located in `AzureAD` area which is part of a framework. Is there an option to write some extension so I can run my SignIn action first and then make a redirect to this one or what do u suggest? – GoldenAge Apr 28 '20 at 08:27
  • You can redirect user in index action , by default it will redirect to index page . Otherwise you should disable the global authentication filter which added by template , and add a login action to triage the aad authentication schema , it will automatically redirect user to the login page in AAD's default login razor pages – Nan Yu Apr 28 '20 at 08:36
  • Mate just wanted to say BIG MASSIVE THANKS! You can't even imagine how many hours I spent trying to solve this issue! Now everything works as I just wanted :) – GoldenAge Apr 28 '20 at 09:55
  • So what would I do if I wanted to do something right after the user is authenticated/logs in? Say, for instance, I wanted to capture the timestamp to save user's log in time? – Michael G Sep 23 '20 at 23:41
  • 1
    @MichaelGervasoni , in OnTokenValidated event , you can perform the database operation – Nan Yu Sep 24 '20 at 05:34
2

Thanks to the Nan Yo answer I managed to establish a redirect to a route I want after I log in via AzureAd and I also managed to make a redirect to a proper page based on the role of a logged user.

I modified the _LoginPartial to be:

@using HighElo.Web.Extensions
<ul class="navbar-nav">
    @if (User.Identity.IsAuthenticated)
    {
        <li class="nav-item">
            <span class="navbar-text text-dark">Logged as: <b>@User.GetEmail()</b></span>
        </li>
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="AzureAD" asp-controller="Account" asp-action="SignOut">Sign out</a>
        </li>
    }
    else
    {
        <li class="nav-item">

            <a class="nav-link text-dark" href="/Account/SignIn">Sign in</a>
        </li>
    }
</ul>

I created a new controller in my root directory, which looks like this:

[AllowAnonymous]
public class AccountController : Controller
{
    [Route("Account/SignIn")]
    public IActionResult SignIn()
    {
        if (!User.Identity.IsAuthenticated)
        {
            return Challenge(new AuthenticationProperties() { RedirectUri = "/Account/Callback" }, AzureADDefaults.AuthenticationScheme);
        }

        return Forbid();
    }

    [Authorize]

    public IActionResult Callback()
    {
        if (User.IsInRole("Client"))
        {
            //redirect to the Clients area
            return LocalRedirect("/Clients/Dashboard");
        }
        //here comes other role checks

        return Forbid();
    }
}
GoldenAge
  • 2,918
  • 5
  • 25
  • 63