Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is ASP.NET Core Identity needed for Intranet app using Windows Authentication

Using Windows Authentication in an Intranet web application I want to achieve the following:

  • Gather additional attributes from AD (name, employee number)
  • Gather additional attributes from a database table (working hours, pay)
  • Authorize based on application roles (not AD groups)
  • Authorize based on an AD attribute (has direct reports)
  • User not provide a username/password

In my search for an answer it is suggested that I need to add ClaimsTransformation to my application:

How do I use Windows Authentication with users in database

Populate custom claim from SQL with Windows Authenticated app in .Net Core

Caching Claims in .net core 2.0

Though I don't fully understand the solution and why ClaimsTransformation happens on every request so I'm looking for answers to the following:

  1. Is ASP.NET Core Identity required for ClaimsTransformation to work?
  2. Does ClaimsTransformation happen on every request with just Windows Authentication or also with form based authentication?
  3. Does this have to happen on every request?
  4. Caching claims like GivenName, Surname seem simple but what about roles? What steps need to be taken to ensure the database isn't hit every time but roles do get updated when there are changes.
  5. Is there a simpler alternative for what I'm trying to do?
like image 978
mheptinstall Avatar asked Apr 10 '18 09:04

mheptinstall


People also ask

What is ASP.NET Core identity used for?

ASP.NET Core Identity: Is an API that supports user interface (UI) login functionality. Manages users, passwords, profile data, roles, claims, tokens, email confirmation, and more.

What is Windows authentication in ASP.NET Core?

Windows Authentication relies on the operating system to authenticate users of ASP.NET Core apps. Windows Authentication is used for servers that run on a corporate network using Active Directory domain identities or Windows accounts to identify users.

What is identity authentication in ASP.NET Core?

It means ASP.NET Core Identity provides a separate storing concept for identity information (like username, password) and code for security implementations (like password hashing, password validation, etc.).

How many types of authentication ASP.NET supports Windows authentication?

ASP.NET supports Forms Authentication, Passport Authentication, and Windows authentication providers. The mode is set to one of the authentication modes: Windows, Forms, Passport, or None. The default is Windows.


2 Answers

This article gave me some ideas and here is a possible solution.

Controllers would inherit from a base controller which has a policy that requires the Authenticated claim. When this isn't present it goes to the AccessDeniedPath and silently performs the login adding the Authenticated claim along with any other claims, if this is already present then the Access Denied message would appear.

When creating the new ClaimsIdentity I've had to strip most of the Claims in the original identity as I was getting a HTTP 400 - Bad Request (Request Header too long) error message.

Are there any obvious issues with this approach?

Startup.cs

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

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();

        services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddCookie(options =>
            {
                options.LoginPath = "/Home/Login";
                options.AccessDeniedPath = "/Home/AccessDenied";
            });

        services.AddAuthorization(options =>
        {
            options.AddPolicy("Authenticated",
                policy => policy.RequireClaim("Authenticated"));
            options.AddPolicy("Admin",
                policy => policy.RequireClaim("Admin"));
        });
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseBrowserLink();
            app.UseDeveloperExceptionPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

        app.UseStaticFiles();
        app.UseAuthentication();

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

Controller

[Authorize(Policy = "Authenticated")]
public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }

    [Authorize(Policy = "Admin")]
    public IActionResult About()
    {
        return View();
    }

    [AllowAnonymous]
    public async Task<IActionResult> Login(string returnUrl)
    {
        var identity = ((ClaimsIdentity)HttpContext.User.Identity);

        var claims = new List<Claim>
        {
            new Claim("Authenticated", "True"),
            new Claim(ClaimTypes.Name,
                identity.FindFirst(c => c.Type == ClaimTypes.Name).Value),
            new Claim(ClaimTypes.PrimarySid,
                identity.FindFirst(c => c.Type == ClaimTypes.PrimarySid).Value)
        };

        var claimsIdentity = new ClaimsIdentity(
            claims,
            identity.AuthenticationType,
            identity.NameClaimType,
            identity.RoleClaimType);

        await HttpContext.SignInAsync(
            CookieAuthenticationDefaults.AuthenticationScheme,
            new ClaimsPrincipal(claimsIdentity),
            new AuthenticationProperties());

        return Redirect(returnUrl);
    }

    [AllowAnonymous]
    public IActionResult AccessDenied(string returnUrl)
    {
        if (User.FindFirst("Authenticated") == null)
            return RedirectToAction("Login", new { returnUrl });

        return View();
    }
}
like image 58
mheptinstall Avatar answered Nov 15 '22 13:11

mheptinstall


Here is an alternative which does use IClaimsTransformation (using .NET 6)

A few notes:

In the ClaimsTransformer class it's essential to clone the existing ClaimsPrincipal and add your Claims to that, rather than trying to modify the existing one. It must then be registered as a singleton in ConfigureServices().

The technique used in mheptinstall's answer to set the AccessDeniedPath won't work here, instead I had to use the UseStatusCodePages() method in order to redirect to a custom page for 403 errors.

The new claim must be created with type newIdentity.RoleClaimType, NOT System.Security.Claims.ClaimTypes.Role, otherwise the AuthorizeAttribute (e.g. [Authorize(Roles = "Admin")]) will not work

Obviously the application will be set up to use Windows Authentication.

ClaimsTransformer.cs

public class ClaimsTransformer : IClaimsTransformation
{
    // Can consume services from DI as needed, including scoped DbContexts
    public ClaimsTransformer(IHttpContextAccessor httpAccessor) { }

    public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        // Clone current identity
        var clone = principal.Clone();
        var newIdentity = (ClaimsIdentity)clone.Identity;

        // Get the username
        var username = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier || c.Type == ClaimTypes.Name).Value;

        if (username == null)
        {
            return principal;
        }

        // Get the user roles from the database using the username we've just obtained
        // Ideally these would be cached where possible
        
        // ...

        // Add role claims to cloned identity
        foreach (var roleName in roleNamesFromDatabase)
        {
            var claim = new Claim(newIdentity.RoleClaimType, roleName);
            newIdentity.AddClaim(claim);
        }

        return clone;
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddAuthentication(IISDefaults.AuthenticationScheme);
    services.AddAuthorization();
    services.AddSingleton<IClaimsTransformation, ClaimsTransformer>();

    services.AddMvc().AddRazorRuntimeCompilation();

    // ...
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseStatusCodePages(async context => {
        if (context.HttpContext.Response.StatusCode == 403)
        {
            context.HttpContext.Response.Redirect("/Home/AccessDenied");
        }
    });

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

    app.UseRouting();

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

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

Example HomeController.cs

[Authorize]
public class HomeController : Controller
{
    public HomeController()
    { }

    public IActionResult Index()
    {
        return View();
    }

    [Authorize(Roles = "Admin")]
    public IActionResult AdminOnly()
    {
        return View();
    }

    [AllowAnonymous]
    public IActionResult AccessDenied()
    {
        return View();
    }
}
like image 22
John M Avatar answered Nov 15 '22 12:11

John M