Asp.Net Core RazorPages AuthorizeFolder with an exceptional page - asp.net-core

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 .

Related

Authorization in ASP.NET Core razor

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")));
});

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.

Authentication for .NET Core Razor Pages application doesn't work for views without an "/Identity" route while using .AddIdentityServerJwt()

Using the .NET Core 3.1 framework, I'm trying to configure a web platform with the following setup:
A Razor Pages application, that acts as the landing page for the platform with features/pages such as advertising the platform, cookie consent, privacy policy, contacts, and the pages that come with Identity (e.g., login, register, manage account).
Authentication for the Razor Pages application is performed in the standard Identity way.
An Angular SPA, that is only accessible after the user is logged in.
OIDC configuration with Identity Server in order to add authentication and authorisation to the Angular SPA.
All of these three components (Razor Pages + Angular + Identity Server) are bundled into one single .NET Core web project. I have also scaffolded Identity so that I am able to customise the look and behaviour of the pages.
I was able to almost configure the application the way I want it, by basically mixing the code of the startup templates of the Razor Pages option (with user accounts stored locally) and the Angular template option (with user accounts stored locally) and with a bit of trial and error and investigation.
The current status of my application is:
The user logs in in the Razor Pages application.
The login is successful and the email is displayed on the navigation bar.
When we navigate to the SPA, my Angular app tries to silently login and is successful:
localhost:5001/Dashboard (Angular SPA home route)
If we navigate to a part of the Razor Pages application that does not have the /Identity route (which is only used for the pages that come with Identity) the cookies appear to no longer contain the right information and I have no session in those routes. This means that, for example, if I am using the SignInManager.IsSignedIn(User) to only display a navigation option to an Administration page that is protected with an options.Conventions.AuthorizePage($"/Administration"), if I am in a URL that has the Identity route, the navigation tab will be displayed, otherwise it will not be displayed:
localhost:5001/Identity/Account/Login
localhost:5001 (Razor Pages application home route)
However, even though the Administration navigation tab is being displayed when I am on a URL that has the /Identity route, if I click on it I will get a 401 unauthorised error, because the Administration page is not preceded by the /Identity route:
localhost:5001/Administration
I have managed to trace the problem to the the AddIdentityServerJwt(). Without this, the login for the Razor Pages application works as intended, but I am obviously unable to use authentication and authorisation with the Angular application afterwards.
I went to check the source code for that method and it turns out that it creates a new IdentityServerJwtPolicySchemeForwardSelector that forwards the JWT policy scheme to the DefaultIdentityUIPathPrefix which, as you might have guessed it, contains only the value "/Identity".
I have configured my Startup class in the following way:
Startup.cs
public void ConfigureServices(IServiceCollection services)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services
.AddDbContext<ApplicationDbContext>(optionsBuilder =>
{
DatabaseProviderFactory
.CreateDatabaseProvider(configuration, optionsBuilder);
});
services
.AddDefaultIdentity<IdentityUser>(options => options.SignIn.RequireConfirmedAccount = true)
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services
.AddIdentityServer()
.AddApiAuthorization<IdentityUser, ApplicationDbContext>();
services
.AddAuthentication()
.AddIdentityServerJwt();
services
.AddControllersWithViews();
services
.AddRazorPages()
.AddRazorPagesOptions(options =>
{
options.Conventions.AuthorizePage($"/Administration");
});
services
.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist";
});
services.AddTransient<IEmailSender, EmailSenderService>();
services.Configure<AuthMessageSenderOptions>(configuration);
services.AddTransient<IProfileService, ProfileService>();
}
public void Configure(IApplicationBuilder applicationBuilder, IWebHostEnvironment webHostEnvironment)
{
SeedData.SeedDatabase(applicationBuilder, configuration);
if (webHostEnvironment.IsDevelopment())
{
applicationBuilder.UseDeveloperExceptionPage();
applicationBuilder.UseDatabaseErrorPage();
}
else
{
applicationBuilder.UseExceptionHandler("/Error");
applicationBuilder.UseHsts();
}
applicationBuilder.UseHttpsRedirection();
applicationBuilder.UseStaticFiles();
applicationBuilder.UseCookiePolicy();
if (!webHostEnvironment.IsDevelopment())
{
applicationBuilder.UseSpaStaticFiles();
}
applicationBuilder.UseRouting();
applicationBuilder.UseAuthentication();
applicationBuilder.UseIdentityServer();
applicationBuilder.UseAuthorization();
applicationBuilder.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller}/{action=Index}/{id?}");
endpoints.MapRazorPages();
});
applicationBuilder.UseSpa(spa =>
{
spa.Options.SourcePath = "ClientApp";
if (webHostEnvironment.IsDevelopment())
{
if (bool.Parse(configuration["DevelopmentConfigurations:UseProxyToSpaDevelopmentServer"]))
{
spa.UseProxyToSpaDevelopmentServer(configuration["DevelopmentConfigurations:ProxyToSpaDevelopmentServerAddress"]);
}
else
{
spa.UseAngularCliServer(npmScript: configuration["DevelopmentConfigurations:AngularCliServerNpmScript"]);
}
}
});
}
How can I configure my application so that the session is available across my entire application and not just on URLs that have the "/Identity" route while maintaining both authentication and authorisation for the Razor Pages application and the Angular application?
I had the same problem and solved it by adding my own PolicyScheme that decides which type of authentication should be used based on the request path. All my razor pages have a path starting with "/Identity" or "/Server" and all other requests should use JWT.
I set this up in ConfigureServices using the collowing coding:
// Add authentication using JWT and add a policy scheme to decide which type of authentication should be used
services.AddAuthentication()
.AddIdentityServerJwt()
.AddPolicyScheme("ApplicationDefinedAuthentication", null, options =>
{
options.ForwardDefaultSelector = (context) =>
{
if (context.Request.Path.StartsWithSegments(new PathString("/Identity"), StringComparison.OrdinalIgnoreCase) ||
context.Request.Path.StartsWithSegments(new PathString("/Server"), StringComparison.OrdinalIgnoreCase))
return IdentityConstants.ApplicationScheme;
else
return IdentityServerJwtConstants.IdentityServerJwtBearerScheme;
};
});
// Use own policy scheme instead of default policy scheme that was set in method AddIdentityServerJwt
services.Configure<AuthenticationOptions>(options => options.DefaultScheme = "ApplicationDefinedAuthentication");

How to allow to unauthenticated users see some specific pages or actions pages in Cake PHP 3?

With CakePHP 3 we used Auth component and this worked like this CakePHP - How to allow unauthenticated access to specific pages
Now I'm trying to use the new Authentication and Authorization plugins instead (I don't know if it is the best solution).
I have this case:
I have some tables in the database for entities (cars, brands, and users). I have users and 4 level user roles (pyramid).
- Admins can change everything
- Editors can see and add brands and cars, but only can edit or update cars and brands created by themselves.
- Registered users can add only cars and edit their cars (and see all cars and brands).
- Anonymous users can see all but only can create a user account.
Authentication works well alone. To allow anonymous user access to content I use $this->Authentication->allowUnauthenticated(['login', 'add']); but when I load Authorization plugin, everything give error.
Do I need to specify all Authorization access with authorizeModel and other functions? There is a way to authorize at the same time with both plugins? Do I really need Authorization plugin for this and is recommended or Authentication plugin can handle this?
With previous Auth component I worked with something like this piece of code:
In AppController.php
public function beforeFilter(Event $event)
{
$this->Auth->allow(['view', 'display']);
}
public function isAuthorized($user)
{
return true;
}
In UsersController.php
public function beforeFilter(Event $event)
{
parent::beforeFilter($event);
$this->Auth->allow('add', 'logout');
}
In Cars and Brands controllers
public function isAuthorized($user)
{
if (isset($authUser['role']) && $authUser['role'] === 'admin') {
return true;
}
if ($this->request->action === 'add') {
return true;
}
if ($this->request->action === 'index') {
return true;
}
if (in_array($this->request->action, ['edit'])) {
$carId = (int)$this->request->params['pass'][0];
if ($this->Cars->exists(['id' => $carId, 'user_id' => $authUser['id']])) {
return true;
}
}
return false;
}
Followed from https://book.cakephp.org/3/es/tutorials-and-examples/blog-auth-example/auth.html
My versions are:
- CakePHP 3.8
- Authentication plugin 1.4
- Authorization plugin 1.3
Sorry if my question is a bit basic but documentation is not very clear with this. I can add more details if needed.
Edit: If I quit unauthenticatedRedirect I get:
No identity found. You can skip this check by configuring `requireIdentity` to be `false`.
Authentication\Authenticator\UnauthenticatedException
If I add requireItentity as false, in AppController
$this->loadComponent('Authentication.Authentication', [
'requireIdentity' => false
]);
I get (where / is the path, can be /cars /brands)
The request to `/` did not apply any authorization checks.
Authorization\Exception\AuthorizationRequiredException
If I use this in AppController (always Authentication before Authorization)
$this->loadComponent('Authentication.Authentication', [
'requireIdentity' => false
]);
$this->loadComponent('Authorization.Authorization', [
'skipAuthorization' => [
'login',
]
]);
and this in Application
$service->setConfig([
'unauthenticatedRedirect' => \Cake\Routing\Router::url('/users/login'),
'queryParam' => 'redirect',
]);
I send all users to login page but authorization checks error appears.
With $this->Authorization->skipAuthorization(); in beforeFilter() user can see the pages and works but I don't know if it is appropriated.
If I use this in any controller beforeFilter $this->Authorization->authorizeModel('index', 'add', 'display' ...);
I get
Policy for `App\Model\Table\CarsTable` has not been defined.
Authorization\Policy\Exception\MissingPolicyException
In home (or pages controller) I get
Policy for `Cake\ORM\Table` has not been defined.
Authorization\Policy\Exception\MissingPolicyException
Do I really need to create policies for each table? I think is more complex than previous Auth component or maybe I'm doing something wrong.

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.