'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 |
