Skip to content

Instantly share code, notes, and snippets.

@DamienBraillard
Last active April 1, 2024 06:22
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save DamienBraillard/4dbd6aa2c56edf5a8e57c59b6e08da94 to your computer and use it in GitHub Desktop.
Save DamienBraillard/4dbd6aa2c56edf5a8e57c59b6e08da94 to your computer and use it in GitHub Desktop.
Asp.Net Core simple role authorization with Windows Authentication

Asp .Net Core simple role authorization with Windows Authentication

What's this all about ?

The idea is to use good old fashioned role based authorization with Windows Authentication.

The default behavior of WindowsAuthentication is to load the claims and roles from the windows authorization pipeline (AD, local rights, etc...). This results in the roles or claims to be based on the user groups.

If this behavior does not suit you and you would prefer loading the user roles from a custom storage (database, configuration file, etc...), continue on reading.

To make this happend, you need to implement a class that loads the roles for a user, enable windows authentication and register the role management services.

Note that this implementation is based on simple roles authorization. You can still use the same principle of registering a IClaimsTransformation that sets up claims that are not used as roles and then define access policies based on these claims.

How to use

Configure windows authentication with IIS or HTTP.sys

First enable the windows authentication option in IIS, IIS Express or HTTP.sys (see link below if you don't know how to do it).

Then, if you are using ASP.Net Core 2.x you must register extra services to perform the authentication challenge. To do so, add one of the following line in the ConfigureServices method.

Using IIS:

// IISDefaults requires the following import:
// using Microsoft.AspNetCore.Server.IISIntegration;
services.AddAuthentication(IISDefaults.AuthenticationScheme);

Using HTTP.sys

// HttpSysDefaults requires the following import:
// using Microsoft.AspNetCore.Server.HttpSys;
services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);

In addition, Do not forget to add app.UseAuthentication(); to the Configure method of your Startup class.

For detailled instructions, please refer to the documentation page provided by microsoft: Configure Windows Authentication in ASP.NET Core

Including the library code

Include the three files into your projetc (don't forget to fix the namespaces):

  • ISimpleRoleProvider.cs
  • SimpleRoleAuthorizationTransform.cs
  • SimpleRoleAuthorizationServiceCollectionExtensions.cs

Implement the class that will load the user roles

To provide the roles for a given user, you must implement a class inheriting from ISimpleRoleProvider. The GetUserRolesAsync method receives the full windows user name including the domain or machine name (eg. MyDomain\Myuser). Beware that it will be called for each request so keep in mind that it might affect performance.

The example below has hard-coded roles for two users. But feel free to load the roles from any data source like a database or the configuration files. Also note that we defined constants for role names. These are not mandatory but can help avoid typos when restricting access to routes later. As constants, they can be used as parameter to the Routes property of the AuthorizeAttribute attribute.

public class DemoSimpleRoleProvider : ISimpleRoleProvider
{
    public const string ADMIN = "Admin";
    public const string BASIC_USER = "BasicUser";

    public Task<ICollection<string>> GetUserRolesAsync(string userName)
    {
        ICollection<string> result = new string[0];

        // Here, John is a basic user and Arnold an admin user
        // Feel free to load roles from any source you like.
        if (!string.IsNullOrEmpty(userName))
        {
            if (userName.EndsWith("john", StringComparison.OrdinalIgnoreCase))
                result = new[] { BASIC_USER };
            else if (userName.EndsWith("arnold", StringComparison.OrdinalIgnoreCase))
                result = new[] { BASIC_USER, ADMIN };
        }

        return Task.FromResult(result);
    }
}

Register the simple role authentication and enable your role provider

The last step to enable simple role authentication is to register the simple role transform and the simple role provider you implemented.

In the ConfigureServices method, add the following code:

// Setup the custom simple role authorization with
// "DemoSimpleRoleProvider" implementation to provide the roles for a user.
services.AddSimpleRoleAuthorization<DemoSimpleRoleProvider>();

Declaring required roles on the Controllers and controller routes

To restrict access to certain roles on your controller and controller routes, you must use the AuthorizeAtribute and set it's Roles property to the roles that allowed to access the controller or method.

Here is an example of a controller restricting access. Note that, as we defined constants for role names in our DemoSimpleRoleProvider, we can use the same constants here:

[Route("api/[controller]")]
[ApiController]
public class DemoController : ControllerBase
{
    [HttpGet]
    public ActionResult<string> Get()
    {
        return "Congrats ! Anyone can access this !";
    }

    [HttpGet("Admin")]
    [Authorize(Roles=DemoSimpleRoleProvider.ADMIN)]
    public ActionResult<string> GetAdmin()
    {
        return "Congrats ! You just made an admin call !";
    }

    [HttpGet("Basic")]
    [Authorize(Roles = DemoSimpleRoleProvider.BASIC_USER)]
    public ActionResult<string> GetBasic()
    {
        return "Congrats ! You just made a basic user call !";
    }
}
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
namespace DamienBraillard.AspNetCoreSimpleRoleAuthorization
{
/// <summary>
/// Defines the base functionality of the class used to provide applicative roles for a user when using the simple role
/// authorization.
/// </summary>
public interface ISimpleRoleProvider
{
#region Public Methods
/// <summary>
/// Loads and returns the role names for a given user name.
/// </summary>
/// <param name="userName">The login name of the user for which to return the roles.</param>
/// <returns>
/// A collection of <see cref="string" /> that describes the roles assigned to the user;
/// An empty collection of no roles are assigned to the user.
/// </returns>
/// <remarks>
/// <para>Beware that this method is called for each controller call. It might impact performance.</para>
/// <para>
/// If Windows authentication is used, the passed <paramref name="userName" />
/// is the full user name including the domain or machine name (e.g "CostroDomain\JohnDoe" or
/// "JOHN-WORKSTATION\JohnDoe").
/// </para>
/// <para>
/// The returned roles names can be used to restrict access to controllers using the <see cref="AuthorizeAttribute" />
/// (<c>[Authorize(Roles="...")]</c>
/// </para>
/// </remarks>
Task<ICollection<string>> GetUserRolesAsync(string userName);
#endregion
}
}
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.DependencyInjection;
namespace DamienBraillard.AspNetCoreSimpleRoleAuthorization
{
/// <summary>
/// Provides the extension methods to enable and register the simple role authentication on an Asp.Net Core web site.
/// </summary>
public static class SimpleRoleAuthorizationServiceCollectionExtensions
{
#region Public Static Methods
/// <summary>
/// Activates simple role authorization for Windows authentication for the ASP.Net Core web site.
/// </summary>
/// <typeparam name="TRoleProvider">The <see cref="Type"/> of the <see cref="ISimpleRoleProvider"/> implementation that will provide user roles.</typeparam>
/// <param name="services">The <see cref="IServiceCollection"/> onto which to register the services.</param>
public static void AddSimpleRoleAuthorization<TRoleProvider>(this IServiceCollection services)
where TRoleProvider : class, ISimpleRoleProvider
{
services.AddSingleton<ISimpleRoleProvider, TRoleProvider>();
services.AddSingleton<IClaimsTransformation, SimpleRoleAuthorizationTransform>();
}
#endregion
}
}
using System;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
namespace DamienBraillard.AspNetCoreSimpleRoleAuthorization
{
/// <summary>
/// Implements a <see cref="IClaimsTransformation" /> that uses a <see cref="ISimpleRoleProvider" /> to fetch and apply
/// applicative roles
/// for a user.
/// <para>
/// To use, you need to implement a class that inherit from <see cref="ISimpleRoleProvider" /> and use the
/// <see cref="SimpleRoleAuthorizationServiceCollectionExtensions.AddSimpleRoleAuthorization{TRoleProvider}" /> extension
/// method
/// in the <c>ConfigureServices</c> method of the <c>Startup</c> class to enable the simple role authorization and
/// associate your simple role provider implementation.
/// </para>
/// </summary>
public class SimpleRoleAuthorizationTransform : IClaimsTransformation
{
#region Private Fields
private static readonly string RoleClaimType = $"http://{typeof(SimpleRoleAuthorizationTransform).FullName.Replace('.', '/')}/role";
private readonly ISimpleRoleProvider _roleProvider;
#endregion
#region Public Constructors
public SimpleRoleAuthorizationTransform(ISimpleRoleProvider roleProvider)
{
_roleProvider = roleProvider ?? throw new ArgumentNullException(nameof(roleProvider));
}
#endregion
#region Public Methods
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
// Cast the principal identity to a Claims identity to access claims etc...
var oldIdentity = (ClaimsIdentity)principal.Identity;
// "Clone" the old identity to avoid nasty side effects.
// NB: We take a chance to replace the claim type used to define the roles with our own.
var newIdentity = new ClaimsIdentity(
oldIdentity.Claims,
oldIdentity.AuthenticationType,
oldIdentity.NameClaimType,
RoleClaimType);
// Fetch the roles for the user and add the claims of the correct type so that roles can be recognized.
var roles = await _roleProvider.GetUserRolesAsync(newIdentity.Name);
newIdentity.AddClaims(roles.Select(r => new Claim(RoleClaimType, r)));
// Create and return a new claims principal
return new ClaimsPrincipal(newIdentity);
}
#endregion
}
}
@prasadd75
Copy link

If you face a problem that TransformAsync is not invoked, add
app.UseAuthentication(); in Configure() inside Startup.cs

@muhammadhassan149
Copy link

Hello Damien,
Hope you are doing well.
Can you please help me with this code snippet. I've implemented all these files but it's not working. Can you please provide the completed project with Simple Role Provider with Windows Authentication.

Thank you

@abmptit
Copy link

abmptit commented Apr 7, 2020

Hi, same here I followed all the steps, nothing...

@acceliance
Copy link

Hi, the same for me

Anyone found the right code ?

@DamienBraillard
Copy link
Author

Hi everyone,

Simply add "app.UseAuthentication()" to the Configure method of your Startup.cs.

Otherwise, follow the guidelines to setup authorization in Asp.Net core. This gist suppose that you know how to do it. If not, there are great resources on the web for that.

Hope it helps.

@acceliance
Copy link

acceliance commented Sep 19, 2020

I created my own lib and the sources everything works and is there
https://www.nuget.org/packages/JT.AspNetBaseRoleProvider/

it took me time to properly setup the code but it works perfectly and the integration is realy smooth

@ehsnipe
Copy link

ehsnipe commented Oct 28, 2020

Thanks, works fine!
But when I try to access my repository from DemoSimpleRoleProvider to get the roles I get:
InvalidOperationException: Cannot consume scoped service 'namspace.IMyRepository' from singleton 'namespace.ISimpleRoleProvider'
I did try to change the services.AddScoped in Startup.Configuration to services.AddSingleton but the the problem moved to my dbContext...

@acceliance
Copy link

@ehsnipe
Copy link

ehsnipe commented Oct 29, 2020

Thanks for answering!
Yes, I did.

My solution was to add the follwing in Star.Configuration

        #region AddSingleton
        //MapToolRoleProvider is a Singleton, therfore the repository that it use to read userRoles from needs to also be Singleton
        //Below we use the AppDbContext inside a scope and return it to a Singleton
        //TODO: UserRoles are only read when application start... Might need a solution for this?
        services.AddSingleton<IUserRoleRepositoryLight>(sp =>
        {
            using (var scope = sp.CreateScope())
            {
                var dbContext = scope.ServiceProvider.GetService<AppDbContext>();
                var lastItem = dbContext.UserRoles
                                .Include(ur => ur.Role)
                                .Include(ur => ur.User)
                                .ToList();

                return new UserRoleRepositoryLight(lastItem);
            }
        });
        #endregion

And add this class (and interface):
public class UserRoleRepositoryLight : IUserRoleRepositoryLight
{
private readonly IEnumerable _userRoles;
public UserRoleRepositoryLight(IEnumerable userRoles)
{
_userRoles = userRoles;
}
public IEnumerable GetAllUserRoles()
{
return _userRoles;
}
}

@acceliance
Copy link

Ok,

Do you mean it works perfectly for you ?

Do you think i should improve my library (for the community :-)

Regards, José

@ehsnipe
Copy link

ehsnipe commented Oct 29, 2020

Hi,
It works for now. I have to see how much a problem the need for restart after changing role is.
I think the library is good. If some one have the same issue as I, then they can look in this comment section :-)

@mrezekiel
Copy link

Thanks heaps for this, works great for setting the roles on controllers, but I'm having difficulties using it in a view, does anyone have any advice?

@SarahAbelWhab
Copy link

Hi ehsnipe,
can u help me, and provide more clarification on how did you access your repository from DemoSimpleRoleProvider after adding the singleton to the configuration

@acceliance
Copy link

Thanks heaps for this, works great for setting the roles on controllers, but I'm having difficulties using it in a view, does anyone have any advice?

Look at https://www.nuget.org/packages/JT.AspNetBaseRoleProvider/ it is a fully functional implementation

@SarahAbelWhab
Copy link

Hi mrezekiel,
you can use something like that "@User.IsInRole(RoleProvider.ADMIN)", you can see it in the project which "Jtotec" provided in the comments.

@SarahAbelWhab
Copy link

@jtotec really thanks for your help.

@acceliance
Copy link

you re welcome :-)

@mrezekiel
Copy link

@SarahAbelWhab
I tried your line without changing anything, works great.

@mrezekiel
Copy link

One more question, someone might be able to help, I have a controller lets call it "books" and I need both "Book Admins" and "Book Users" to access the books controller, what's the best method to handle this, users in either group a OR group b get authorized?

@mrezekiel
Copy link

The very last issue I'm having trouble with, if anyone can help, seems there is some type of caching going on and a restart of the application is required when roles are changed, is this true or have I done something wrong? If it is caching by default, can I create a page that will clear the cache and re-auth someone?

@perwillneratpulsen
Copy link

"some type of caching going"
Yes, when it is AddSingleton then the resource is only created once at stratup.
What I did was when I add or remove a role to a user then I also add or remove the role to UserRoleRepositoryLight

@mrezekiel
Copy link

That sounds perfect actually, I’ll just have an add/remove role method accessible externally, thanks.

@dlj23
Copy link

dlj23 commented Jul 8, 2021

Can someone provide the full solution on how to use the UserRoleRepositoryLight method?

I've used the libraries and setup the simple role. Hardcoding is fine, but when i try to access my database of users i get the scoped vs singleton error.

@andfae
Copy link

andfae commented Nov 19, 2021

I have problems in the DemoSimpleRoleProvider to access a dbContext for retrive usersrole from database not hardcoded. Can someone clarify how to use dbcontext inside this method? thank you

@AsIndeed
Copy link

Hello, I followed the steps but I'm getting this error.:
InvalidOperationException: No authenticationScheme was specified, and there was no DefaultChallengeScheme found. The default schemes can be set using either AddAuthentication(string defaultScheme) or AddAuthentication(Action configureOptions).

With services.AddAuthentication(IISDefaults.AuthenticationScheme); line I should have added a defaultScheme but it doesn't work. Anyone can help me with that?

@lscorcia
Copy link

I've been using this for a while now and it works great 99% of the time, but I think I just traced a bad side effect to it (I'm still not entirely sure about this). Replacing the current ClaimsIdentity seems to cause issues regarding the CSRF token. If reauthentication is triggered for any reason, the identity is replaced with a new object and this seems to invalidate any previous CSRF token.

@SaudiRoid
Copy link

i use this code and work fine but my AccessDeniedPath not working and if user no has permission the browser show 403 error

How i can configure the AccessDeniedPath with this codes ??

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment