'Authorize attribute with role redirects to Access denied

I have simple ASP .NET MVC application. I'm trying to do authentication and authorization.

For authentication I have setup Azure AD and I'm adding role to the HttpContext.User using a middleware. I know that the role is added because on the View if I do conditional rendering with User.IsInRole("Admin") the UI is correctly rendered. But the controller with Authorize[Roles = "Admin"] redirects me to

http://localhost:5433/MicrosoftIdentity/Account/AccessDenied?ReturnUrl=%2FHome%2FAdd

I even tired Authorize[Policy = "Admin"] but the result is same.

From services if I remove AddMicrosoftIdentityUI() then it is redirected to

http://localhost:5433/Account/AccessDenied?ReturnUrl=%2FHome%2FAdd

I don't know how does the Authorize attributes work. Does it not check User.IsInRole("Admin")? Please let me know

Also for me the roles are added in UserDetails table and I do have a role as 'Admin'

Also I could have written a custom attribute and make it work. I just want to know what is the real issue right now.

Code:

Startup.cs

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

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContextPool<EmpDBContext>(
            options => options.UseSqlServer(Configuration.GetConnectionString("EmployeeDBConnection")));

            services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAd");


            services.AddControllersWithViews(options =>
            {
                var policy = new AuthorizationPolicyBuilder()
                 .RequireAuthenticatedUser()
                 .Build();
                options.Filters.Add(new AuthorizeFilter(policy));
            }).AddMicrosoftIdentityUI().AddNewtonsoftJson();

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

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

            app.UseRouting();

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

            app.UseAddClaimsFromDb(); // CUSTOM MIDDLEWARE TO ADD ROLE TO USER

            // CHECKING IF ROLE WAS ADDED
            app.Use(async (context, next) =>
            {
                if (context.User != null && context.User.Identity.IsAuthenticated)
                {
                    bool x = context.User.IsInRole("Admin");
                    System.Console.WriteLine(x); // RETURNS TRUE

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

UseAddClaimsFromDb (AddClaimMiddleware.cs)

public class AddClaimMiddleware
    {
        private readonly RequestDelegate next;

        public AddClaimMiddleware(RequestDelegate next)
        {
            this.next = next;
        }

        public async Task InvokeAsync(HttpContext httpContext, EmpDBContext dBContext)
        {
            var user = httpContext.User;

            // does not perist between request
            if(user != null && user.Identity.IsAuthenticated)
            {
                string email = user.Identity.Name;

                UserDetail userDetail = await dBContext.UserDetails.FirstOrDefaultAsync(e => e.EmailId == email);
                string role = userDetail?.Role ?? "Employee";

                ClaimsIdentity claimsIdentity = new ClaimsIdentity();
                claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, role));

                user.AddIdentity(claimsIdentity);

               // httpContext.Session.SetString("employeeId", userDetail.EmployeeId.ToString());

            }

            await next(httpContext);
        }
    }


Solution 1:[1]

I have a similar approach but I'm using Blazor WASM with Azure AD B2C.

In order to add the roles claims to the identity of the current user you need to overwrite the existing identity (ClaimPrincipal) in order to have a single identity with the roles required.

I use this code:

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

            // Support AD and local accounts
            var nameId = principal.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier ||
                                                              c.Type == ClaimTypes.Name);
            if (nameId == null)
            {
                return principal;
            }

            // Get user from database
            var userResponse = await _userService.GetAsync(nameId.Value);
            if (!userResponse.Succeeded || userResponse.Data is null)
            {
                return principal;
            }

            var user = userResponse.Data;
            var rolesResponse = await _userService.GetRolesAsync(user.Id);

            if (rolesResponse.Succeeded && rolesResponse.Data is not null)
            {
                // Add role claims to cloned identity
                foreach (var role in rolesResponse.Data.UserRoles.Where(w => w.Selected).ToList())
                {
                    var claim = new Claim(newIdentity.RoleClaimType, role.RoleName);
                    newIdentity.AddClaim(claim);
                }
            }

            return clone;
        }

I apply this transformation in my Toekn Validation, but in your case I think should be injected in the middleware part.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Nicola Biada