Authorization in ASP.NET Core razor - asp.net-core

I have an admin area I want every user except normal user to be able o go to the admin area.
My user types are dynamic - what should I do?
My authentication is permission based.
I used this code, but it is for static account types:
services.AddAuthorization(options =>
{
builder => builder.RequireRole(Roles.Administrator, Roles.ContentUploader));
options.AddPolicy("Discount",
builder => builder.RequireRole(Roles.Administrator));
});

According to your description, I suggest you could write a custom policy like below to match your requirement.
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("NonNormalUser", policy =>
policy.RequireAssertion(context =>
!context.User.IsInRole("Normal")));
});

Related

How do I authorize based on nested Azure AD groups?

I'm working on an ASP.NET Core MVC website for internal use at my company. It contains a variety of web-based tools, each of which has its own set of permissions for who can access it. My authorization is based on Azure Active Directory groups. My setup has been working well for authorizing based on direct members of groups, but now I want to start using nested groups, and it's not working.
In the example below, I have the group "View Report" which determines who has access to view a particular report. This group contains a handful of individuals, plus a group that contains the entire IT team.
Unfortunately, this approach isn't working because my group claims don't include the View Report group, because I'm not a direct member.
Here is my authentication setup code in Program.cs:
// Sign in users with the Microsoft identity platform
var initialScopes = new string[] { "User.Read", "GroupMember.Read.All" };
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
options =>
{
builder.Configuration.Bind("AzureAd", options);
options.Events = new OpenIdConnectEvents();
}, options => { builder.Configuration.Bind("AzureAd", options); })
.EnableTokenAcquisitionToCallDownstreamApi(options => builder.Configuration.Bind("AzureAd", options), initialScopes)
.AddMicrosoftGraph(builder.Configuration.GetSection("GraphAPI"))
.AddInMemoryTokenCaches();
// Add policies to authorize by Active Directory groups
builder.Services.AddAuthorization(options =>
{
AddPoliciesForEachGroup(options);
});
void AddPoliciesForEachGroup(AuthorizationOptions options)
{
var allGroups = builder.Configuration.GetSection("Groups").GetChildren().ToDictionary(x => x.Key, x => x.Value);
foreach (var group in allGroups)
{
options.AddPolicy(group.Key, policy =>
policy.RequireAssertion(context => context.User.HasClaim(c =>
c.Type == "groups" && c.Value == group.Value)));
}
}
Relevant part of appsettings:
"Groups": {
"ViewReport": "5daa2626-5352-441d-98cc-0b59589dbc6d"
// other groups for other tools...
}
I'm not sure what to do about this. Is there any way to include nested groups in my user claims? Or am I completely off base with this entire approach? I don't know a whole lot about Azure AD (I just followed a tutorial to achieve my current setup) and I acknowledge that I'm lacking a lot of foundational knowledge, so I'm hoping someone can at least point me in the right direction for how to solve this problem, even if it involves a totally different approach to authentication.
(Note: I know that I can achieve authentication for multiple groups by making the code check for multiple specific groups, but I would prefer a solution that allows me to freely add and remove groups in AAD without making code/configuration changes.)
Here's a sample.
In Program.cs:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration)
.EnableTokenAcquisitionToCallDownstreamApi(new string[] { "User.ReadWrite.All" })
.AddMicrosoftGraph(builder.Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches()
.AddDownstreamWebApi("DownstreamApi", builder.Configuration.GetSection("DownstreamApi"))
.AddDistributedTokenCaches();
builder.Services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.TokenValidationParameters.RoleClaimType = "groups";
});
// Adding authorization policies that enforce authorization using Azure AD roles.
builder.Services.AddAuthorization(options =>
{
// this policy stipulates that users in both GroupMember and GroupAdmin can access resources
options.AddPolicy("MemberGroupRequired", policy => policy.RequireRole(builder.Configuration["Groups:GroupMember"], builder.Configuration["Groups:GroupAdmin"]));
// this policy stipulates that users in GroupAdmin can access resources
options.AddPolicy("AdminGroupRequired", policy => policy.RequireRole(builder.Configuration["Groups:GroupAdmin"]));
});
In appsetting.json:
"Groups": {
"GroupAdmin": "group_object_id",
"GroupMember": "group_object_id"
},
In Controller, add attribute [Authorize(Policy = "AdminGroupRequired")] before action method.
With this code, when my user account is a member of "GroupAdmin", then I can access this action. When my user account is a member of the nested group("GroupMember" in my demo) of GroupAdmin, I can access this action as well. But if my user account is a member of neither "GroupAdmin" or "GroupMember", then I can't access this action method.
This method is adding all allowed groups into one policy. In your scenario, you should set object id of "ViewReport" and "entire IT team" group.

ASP.NET Core 3.1 web application use authorization for multiple areas using different authentication types

I have an ASP.NET Core 3.1 application which follows domain driven architecture and it has 2 areas, one for admin and other one for customers (application users).
I want to enable authentication and authorization for each area separately. For example use Identity 4 for the customer area and cookie base authentication for admin area. But it should be done using a single database and role base authentication should not used to separate areas.
What is the best approach to follow. For example "Multiple authentication scheme", Or any other method.
When it comes to login for admin and customer you can implement it by using acr_values (see definition in spec). Identity server can decide how to authenticate based on acr_values, for example if you provided admin_login as acr_values, then based on that Identity Server will authenticate user (use different identity provider or different database/table).
Your application needs to know whether user wants to login as customer or admin before you redirect to identity server authorize endpoint. In order to know that you will have to implement different authentication schemes in your application (one for admin and one for customer). Once you know user login type, you can add correct acr_values. Below code is not tested but it should give you an idea on how to implement it.
services.AddAuthentication(options =>
{
options.DefaultScheme = "CustomerCookie";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("CustomerCookie", options =>
{
options.Cookie.Name = "CustomerCookie";
options.ForwardChallenge = "oidc";
})
.AddCookie("AdminCookie", options =>
{
options.Cookie.Name = "AdminCookie";
options.ForwardChallenge = "admin-oidc";
})
.AddOpenIdConnect("oidc", options =>
{
// Configure all other options needed.
options.SignInScheme = "CustomerCookie";
options.CallbackPath = "/signin-oidc-customer";
options.Events.OnRedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.SetParameter("acr_values", "customer_login");
return Task.FromResult(0);
};
})
.AddOpenIdConnect("admin-oidc", options =>
{
// Configure all other options needed.
options.SignInScheme = "AdminCookie";
options.CallbackPath = "/signin-oidc-admin";
options.Events.OnRedirectToIdentityProvider = (context) =>
{
context.ProtocolMessage.SetParameter("acr_values", "admin_login");
return Task.FromResult(0);
};
});
On identity server side you have full control on what to do based on acr_values, you can use external provider for admin.
You could use IIdentityServerInteractionService.GetAuthorizationContextAsync to retrieve acr_values and you could implement IProfileService so that once authenticated, you can decide what claims to include based on user type (admin or customer).
That would be the basic idea, hopefully it is useful.

Asp.Net Core RazorPages AuthorizeFolder with an exceptional page

In RazorPagesOptions in asp.net core 3,
I authorized a folder FolderA with policy PolicyA like below.
options.Conventions.AuthorizeFolder("/FolderA", "PolicyA");
But I want to apply an exceptional policy PolicyB to a page PageB under FolderA,
so that users who entitled PolicyB but not entitled PolicyA can access PageB
while cannot access the other pages under FolderA.
I tried to add a page authorization like below
options.Conventions.AuthorizePage("/FolderA/PageB", "PolicyB");
but, it does not allow users who entitled PolicyB without PolicyA to access the the PageB.
Is there a way to give a policy exception to a specific page under a authorized folder?
As a workaround, you can dynamically check the path in policy to allow the specific page . For example :
services.AddRazorPages().AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizeFolder("/FolderA", "PolicyA");
});
services.AddAuthorization(options =>
{
options.AddPolicy("PolicyA", policy =>
{
policy.RequireAssertion(context =>
{
var resource = context.Resource.ToString();
if (resource.Equals("/FolderA/PageB"))
{
return true;
}
return false;
});
});
});
So that any request to FolderA will fire the policy , you can then add your custom logic in policy , for example , check user claim from context.User.Claims .

Authorization Role/Policy Attributes Not Working In .Net Core 3

I've had no luck getting any Role or Policy attributes working in .Net Core 3. I started my project with the .Net Core Angular starter project with authentication.
I figured this was something to do with the new .AddDefault methods so I have simplified it as much as I possibly can and it still doesn't work.
Here is my policy:
services.AddAuthorization(options =>
{
options.AddPolicy("IsAdmin", policy =>
policy.RequireClaim("role", "admin"));
});
Here is my controller:
[Authorize(Policy = "IsAdmin")]
[Route("api/[controller]")]
public class AdminController : Controller
{
...
I made a custom Profile service that adds the claim to the token,
var claims = new List<Claim>();
if (await _userManager.IsInRoleAsync(user, "Admin"))
{
claims.Add(new Claim(JwtClaimTypes.Role, "admin"));
}
context.IssuedClaims.AddRange(claims);
Inside my access token (from jwt.io):
Other parts of configure services:
services.AddDefaultIdentity<ApplicationUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
...
services.AddAuthentication()
.AddIdentityServerJwt();
The plain [Authorize] tag is working fine with the access token on other controllers.
When I hit this controller with the access token I get a 403 response
What am I missing that is preventing this from working?
I try your code and find that the role claim key has been transformed to the standard Role ClaimsType : http://schemas.microsoft.com/ws/2008/06/identity/claims/role
So using ClaimTypes.Role will fix the problem:
services.AddAuthorization(options => {
options.AddPolicy("IsAdmin", policy =>
{
policy.RequireClaim(ClaimTypes.Role,"admin");
});
});
Demo
You should also be able to achieve this without needing a policy. ASP.NET automatically maps common claims to the Microsoft schema.
When you inspect your access token. You will see you are sending the role claim. But when you look at the claims in the controller, you will notice that it has been transformed to http://schemas.microsoft.com/ws/2008/06/identity/claims/role.
There are two things you can do. Either set the RoleClaimType to ClaimTypes.Role. Like so:
services.Configure<JwtBearerOptions>(IdentityServerJwtConstants.IdentityServerJwtBearerScheme, options => {
options.TokenValidationParameters.RoleClaimType = ClaimTypes.Role;
});
Or tell the JwtSecurityTokenHandler not to map default inbound claims like this:
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
Since it's a static property this can be done at any time. But I set it somewhere during my service registrations.

Asp.net Core 2 AddAuthorization causing 500 internal server error - problems in Startup

I have a project which i have configured to use JWT for authentication. FOr authorization I want to use role based policies. All good so I make the changes to my Startup.cs.
I have in "ConfigureServices" the following added service.
// Use policy auth.
services.AddAuthorization(options =>
{
options.AddPolicy("DisneyUser", policy => policy.RequireClaim("DisneyCharacter", "IAmMickey"));
});
and this works when I go get a JWT token.
If I change it to
// Use policy auth.
services.AddAuthorization(options =>
{
options.AddPolicy("DisneyUser", policy => policy.RequireClaim("DisneyCharacter", "IAmMickey"));
options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "Admin"));
options.AddPolicy("EmployeeAccess", policy => policy.RequireClaim(ClaimTypes.Role, "Employee"));
});
its still works.
However if I removed the first policy line and I am left with:
// Use policy auth.
services.AddAuthorization(options =>
{
options.AddPolicy("AdminOnly", policy => policy.RequireClaim(ClaimTypes.Role, "Admin"));
options.AddPolicy("EmployeeAccess", policy => policy.RequireClaim(ClaimTypes.Role, "Employee"));
});
I get the following status when I go get a JWT token..
500 Internal Server Error.
Wonder if anybody might have a clue as to why having these two options only breaks it?