Authorize for all Gets on webApi NetCore - asp.net-core

I don't know if it's possible, but, could I make something like a default policy for all my GET actions in my webApi, so they require a especific role, one for "read", "write" and "delete".
Not putting a [Authorize()] in each action of a component, more like a policy in the AddAuthorization of the StartUp.
Thanks.

I don't know if it's possible, but, could I make something like a
default policy for all my GET actions in my webApi, so they require a
especific role, one for "read", "write" and "delete".
Yes, you can use the Policy-based authorization with Role-based authorization.
Not putting a [Authorize()] in each action of a component, more like a
policy in the AddAuthorization of the StartUp.
The services.AddAuthorization() method in the Startup.ConfigureServices method is used to define the policies, after that the policies are applied to controllers by using the [Authorize] attribute with the policy name.
Here is a sample about using the Policy-based authorization with Role-based authorization in Asp.net Core MVC, you could refer it.
Create an ASP.NET MVC Core Application (using Individual User Accounts Authentication).
Using migration to generate the Identity User tables.
Click on the project and select add, then add a scaffolding item and select Identity, select Override all files, and Add the Identity Template pages.
Create a view and controller for the Roles:
public class RoleController : Controller
{
RoleManager<IdentityRole> roleManager;
public RoleController(RoleManager<IdentityRole> roleManager)
{
this.roleManager = roleManager;
}
public IActionResult Index()
{
var roles = roleManager.Roles.ToList();
return View(roles);
}
public IActionResult Create()
{
return View(new IdentityRole());
}
[HttpPost]
public async Task<IActionResult> Create(IdentityRole role)
{
await roleManager.CreateAsync(role);
return RedirectToAction("Index");
}
}
using RoleManager to manage roles, add related views.
modify the Startup.cs files, as shown below:
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDatabaseDeveloperPageExceptionFilter();
services.AddIdentity<IdentityUser, IdentityRole>(options => options.SignIn.RequireConfirmedAccount = true)
.AddDefaultUI()
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
services.AddControllersWithViews();
services.AddRazorPages();
In the Register page, you could add a property to let use select the Role, and use the UserManager.AddToRoleAsync() method to add roles for user.
After that, we could create roles suing the RoleController, like this:
Configure Authentication: define he following policies in ConfigureServices:
services.AddAuthorization(options => {
options.AddPolicy("readpolicy",
builder => builder.RequireRole("Admin", "Manager", "User"));
options.AddPolicy("writepolicy",
builder => builder.RequireRole("Admin", "Manager"));
});
Then, you could apply these policies to the Controller or Action method.
[Authorize(Policy = "readpolicy")]
public IActionResult Index()
{
var roles = roleManager.Roles.ToList();
return View(roles);
}
//[Authorize(Policy = "writepolicy")]
[Authorize(Roles ="User")]
public IActionResult Create()
{
return View(new IdentityRole());
}
After that, if current user role doesn't have access to the related action method, it will show the "Access denied":
Reference:
Adding Role Authorization to a ASP.NET MVC Core Application
Policy-Based And Role-Based Authorization In ASP.NET Core 3.0 Using Custom Handler
Policy-based authorization in ASP.NET Core

Related

A single login page for multiple authentication types (including Azure AD) in .NET Core MVC

I have a .NET Core 2.2 MVC web-application. And I've added two authentication types/providers there:
Login/password with local users database (custom thing, without .NET Core Identity)
Azure AD
My goal is to have a login page at /account/login where users can choose between these two authentications and log-in with either of those. So every time an unauthenticated user would open any page (from a controller with [Authorize] attrubite), he would get redirected to /account/login page, which has a login/password web-form with its own submit button, and additionally a Office 365 login link/button.
Just to make it clear - I don't want a custom Microsoft sign-in / Azure AD page. I only want unauthenticated users to get my login page first, from where they can either log-in using my web-form or click on Office 365 login and get to Microsoft sign-in page.
Now, the authentication part is done and seems to work fine, I can log-in with either of authentications, but my plan with redirecting unauthenticated user to /account/login failed. What happens instead is that user is being redirected to Microsoft sign-in page right away. So it looks like Azure AD authentication has a higher priority somehow.
Here's my implementation.
Startup.cs:
// ...
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
options.MinimumSameSitePolicy = SameSiteMode.None;
});
// the presence of CookieAuthenticationDefaults.AuthenticationScheme doesn't seem to influence anything
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// makes no difference either
//services.AddAuthentication(
// options =>
// {
// options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultChallengeScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
// }
//)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, // and it also can be omitted here
options =>
{
options.LoginPath = "/Account/Login";
options.ExpireTimeSpan = TimeSpan.FromDays(45);
})
.AddAzureAD(options => _configuration.Bind("AzureAD", options));
services.AddAuthorization(options =>
{
// as the default policy, it applies to all [Authorize] controllers
options.DefaultPolicy = new AuthorizationPolicyBuilder(
CookieAuthenticationDefaults.AuthenticationScheme,
AzureADDefaults.AuthenticationScheme
)
.RequireAuthenticatedUser() // a simple policy that only requires a user to be authenticated
.Build();
});
services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
{
options.Authority = options.Authority + "/v2.0/";
options.TokenValidationParameters.ValidateIssuer = false;
});
services.AddMvc(options =>
{
// it is my understanding that there is no need create a policy here
// and perform "options.Filters.Add(new AuthorizeFilter(policy))",
// because the default policy is already added and controllers have explicit [Authorize] attribute
// [...] well, actually I tried that too, but it didn't change anything
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
public void Configure(
IApplicationBuilder app,
IHostingEnvironment env,
ILoggerFactory loggerFactory
)
{
// ...
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}"
);
});
}
AccountController.cs:
[Authorize]
[Route("account")]
public class AccountController : Controller
{
// ...
// that is where "Office 365 login" link leads
[HttpGet("login-ad")]
[AllowAnonymous]
public IActionResult LoginAD(string returnUrl = null)
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Account");
}
else
{
if (string.IsNullOrEmpty(returnUrl)) { returnUrl = "/"; }
return Challenge(
new AuthenticationProperties { RedirectUri = returnUrl },
AzureADDefaults.AuthenticationScheme
);
}
}
[HttpGet("login")]
[AllowAnonymous]
public IActionResult Login(string returnUrl = null)
{
if (User.Identity.IsAuthenticated)
{
return RedirectToAction("Index", "Account");
}
ViewData["ReturnUrl"] = returnUrl;
return View();
}
// that is where login/password web-form submits to
[HttpPost("login")]
[AllowAnonymous]
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
{
if (ModelState.IsValid)
{
await _usersManager.SignIn(
model.Login,
model.Password
);
// ...
return LocalRedirect(returnUrl);
}
ViewData["ReturnUrl"] = returnUrl;
return View(model);
}
// ...
}
HomeController.cs:
[Authorize]
public class HomeController : Controller
{
// ...
public IActionResult Index()
{
return View();
}
// ...
}
So, opening any page by an unauthenticated user results in immediate redirect to Microsoft sign-in page. And in order to get to /account/login (to have a chance to log-in using another authentication) users have to open that URL explicitly.
If I remove AzureADDefaults.AuthenticationScheme from default policy, then all unauthenticated requests will now get redirected to /account/login - exactly what I want - but naturally Azure AD authentication doesn't work anymore:
These redirects tell me that after successful authentication at Microsoft sign-in page it returns user back to /account/login, but user is still not authenticated on my website.
I can of course add [AllowAnonymous] to Index action of HomeController and return redirect to /account/login for unauthenticated users, but that obviously would only work for / route.
I have a feeling that I don't understand some things about AddAuthentication(), schemes and policies, thus apparently I did something wrong in Startup.cs. Can you please help me to understand what's wrong there? Or maybe there is some other way to achieve what I want?
Updated answer
I decided to clone the example project mentioned here in the quickstart-v2-aspnet-core-webapp documentation and see if I could reproduce your error.
After cloning the project I added two NuGet packages.
Microsoft.AspNetCore.Identity 2.2.0
Microsoft.AspNetCore.Identity.EntityFrameworkCore 2.2.0
Then added the database context that extends IdentityContext.
ApplicationDbContext.cs
In Startup.cs
Registered Identity
Registered the database context and provided connection string
In AppSettings.json
Configured TenantID and ClientID
Ran the application.
At this point, the app launches and redirects me to Account/Login, where I choose Sign in via Microsoft account.
Now, I can obviously see there is something wrong. It wouldn't authenticate the user.
Turns out:
The extension method .AddAzureAd() actually cannot be used in combination with other authentication methods. See this issue on github.
But luckily the workaround is fairly simple. Just switch out .AddAzureAd() for .AddOpenIdConnect() and change your AppSettings' AzureAd section to:
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Authority": "https://login.microsoftonline.com/{tenantID}/v2.0/",
"TenantId": "{tenantID}",
"ClientId": "{clientID}",
"CallbackPath": "/signin-oidc"
},
Now I can log in perfectly fine with AzureAD and local user accounts as well.
For your convenience, I uploaded the complete example project to my GitHub page.

Assigning a user to a Role inside asp.net core will return this error "You do not have access to this resource."

I created a new asp.net core web application which uses individual user accounts. now i am trying to implement a simple role assignment scenario.
so i register a test user, where the user got added inside the AspNetUser table:-
then i add a new Role named "Administrator" inside the AspNetRole:-
then i added a new AspNetUserRole to link the user to the Role:-
then i added the following Authorize annotation on the About action method:-
[Authorize(Roles = "Administrator")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
but when i try to access the About action method using the user, i got this error:-
You do not have access to this resource."
EDIT
Here is the startup.cs , which i have not modified, so i think it contain the built-in code:-
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using WebApplication2.Data;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace WebApplication2
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.Configure<CookiePolicyOptions>(options =>
{
// This lambda determines whether user consent for non-essential cookies is needed for a given request.
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(
Configuration.GetConnectionString("DefaultConnection")));
services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseCookiePolicy();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}
I guess you manually create role and link role in AspNetUserRoletable after creating your user . Please don't forget to Logout user and login again , so role claims will get/update the new added role .
Your identity service is not configured for roles. AddDefaultIdentity cannot handle roles. You need AddIdentity
Instead of:
services.AddDefaultIdentity<IdentityUser>().AddEntityFrameworkStores<ApplicationDbContext>();
Try:
services.AddIdentity<IdentityUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders();
Short answer
Add IdentityRole :
services.AddDefaultIdentity<IdentityUser>()
.AddRoles<IdentityRole>()
.AddEntityFrameworkStores<ApplicationDbContext>();
Long Answer
For properly using of roles/policies, you need to follow the below steps:
configure ApplicationDbContext for using IdentityRole
configure Identity service to use IdentityRole
configure application cookie
define authorization policies
configure authorization for razor pages
Notice : if you are using razor pages, Authorization attributes must be applied to the PageModel model not the actions
before proceeding with the solution, it is worth to mention that it is a best practice to use custom user and role models instead of IdentityUser and IdentityModel. This will help you add custom fields to the user and role easily.
So, first lets create our custom user and role models:
public class AppUser : IdentityUser
{
//custom fields can be defined here
}
public class AppRole : IdentityRole
{
//custom fields can be defined here
}
public class AppUserRole : IdentityUserRole<string>
{
public virtual AppUser User { get; set; }
public virtual AppRole Role { get; set; }
}
Now we can start with configuring ApplicationDbContext:
public class ApplicationDbContext : IdentityDbContext<AppUser, AppRole, string, IdentityUserClaim<string>, AppUserRole, IdentityUserLogin<string>, IdentityRoleClaim<string>, IdentityUserToken<string>>
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
// Customize the ASP.NET Identity model and override the defaults if needed.
// For example, you can rename the ASP.NET Identity table names and more.
// Add your customizations after calling base.OnModelCreating(builder);
// AppUserRole relationship solution from so
// https://stackoverflow.com/questions/51004516/net-core-2-1-identity-get-all-users-with-their-associated-roles/51005445#51005445
builder.Entity<AppUserRole>(userRole =>
{
userRole.HasKey(ur => new { ur.UserId, ur.RoleId });
userRole.HasOne(ur => ur.Role)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.RoleId)
.IsRequired();
userRole.HasOne(ur => ur.User)
.WithMany(r => r.UserRoles)
.HasForeignKey(ur => ur.UserId)
.IsRequired();
});
}
}
}
configuring Identity
services.AddIdentity<AppUser, AppRole>(ops =>
{
ops.SignIn.RequireConfirmedEmail = true;
// Lockout settings
ops.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30);
ops.Lockout.MaxFailedAccessAttempts = 9;
ops.Lockout.AllowedForNewUsers = true;
// User settings
ops.User.RequireUniqueEmail = true;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
configure application cookie
services.ConfigureApplicationCookie(ops =>
{
// Cookie settings
ops.Cookie.HttpOnly = false;
ops.ExpireTimeSpan = TimeSpan.FromMinutes(30);
// If the LoginPath isn't set, ASP.NET Core defaults the path to /Account/Login.
ops.LoginPath = $"/Identity/Account/Login";
// If the AccessDeniedPath isn't set, ASP.NET Core defaults the path to /Account/AccessDenied.
ops.AccessDeniedPath = $"/Identity/Account/AccessDenied";
ops.SlidingExpiration = true;
});
define authorization policies
services.AddAuthorization(ops =>
{
ops.AddPolicy("Administrator", policy =>
{
policy.RequireRole("Administrator");
});
});
Now it is possible to use roles/policies in different ways:
1- define authorization policies in startup
services.AddMvc()
.AddRazorPagesOptions(ops =>
{
ops.Conventions.AuthorizeFolder("/", "Administrator");
})
.SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
2- apply authorization attributes on actions in case of MVC
[Authorize(Roles = "Administrator")]
public IActionResult About()
{
ViewData["Message"] = "Your application description page.";
return View();
}
3- or apply policy on PageModel for Razor Pages
[Authorize(Policy = "Administrator")]
public class AboutModel : PageModel
{
//-----
}
[UPDATE]
following to your comment below;
Let's consider that you will develop a news website management panel; basically you will need roles like Admins to manage the site settings and Authors to post the news pages, and probably Managers to approve the posted news. With this scenario you can survive with the default Identity settings and role based authorization.
But for example; if you need to allow only authors with more than 100 posted articles and are older than 25 to be able to approve their posts without the Managers approval then you need to customize the IdentityUser and use policy/claim based authorization, in this case the long answer will help you more to develop the application.
you can read more about authorization in the docs

Multiple authentication methods in asp.Net core 2.2

Is there a way to use JWT bearer authentication AND a custom authentication method in .net core? I want all actions to default to JWT, except in a few cases where I want to use a custom authentication header.
I finally figured out how to do it. This example uses JWT authentication by default and custom authentication in certain rare cases. Please note, from what I've read, Microsoft seems to discourage writing your own auth. Please use at your own risk.
First, add this code to the startup.cs ConfigureServices method to ensure that authentication gets applied globally.
services.AddMvc(options =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
options.Filters.Add(new AuthorizeFilter(policy));
})
Then, add this to configure the schemes you wish to use (in our case JWT and Custom).
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
// Jwt Authentication
.AddJwtBearer(options =>
{
options.Audience = ".......";
options.Authority = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_...";
})
// Custom auth
.AddScheme<CustomAuthOptions,
CustomAuthHandler>(CustomAuthOptions.DefaultScheme, options => { });
Next create a class to hold your custom authentication options:
public class CustomAuthOptions : AuthenticationSchemeOptions
{
public const string Scheme = "custom auth";
public const string CustomAuthType = "custom auth type";
}
Finally, add an authentication handler to implement the custom authentication logic.
public class CustomAuthHandler : AuthenticationHandler<CustomAuthOptions>
{
public CustomAuthHandler(
IOptionsMonitor<CustomAuthOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock) : base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
// Auth logic goes here
if (!Request.Headers....)
{
return Task.FromResult(AuthenticateResult.Fail("Authentication Failed."));
}
// Create authenticated user
ClaimsPrincipal principal = .... ;
List<ClaimsIdentity> identities =
new List<ClaimsIdentity> {
new ClaimsIdentity(CustomAuthOptions.CustomAuthType)};
AuthenticationTicket ticket =
new AuthenticationTicket(
new ClaimsPrincipal(identities), CustomAuthOptions.Scheme);
return Task.FromResult(AuthenticateResult.Success(ticket));
}
}
Finally, to tie it all together, add an authorize attribute to the actions you wish to use custom authorization on.
[HttpGet]
[Authorize(AuthenticationSchemes = CustomAuthOptions.Scheme)]
public HttpResponseMessage Get()
{
....
}
Now JWT authentication will automatically get applied to all actions, and custom authentication will get added to only the actions with the Authorize attribute set to the custom scheme.
I hope this helps someone.

Adding authorization in ASP.NET Core application

I have noticed there are two different ways the asp.net core application can be globally configured to Authorize only authenticated user. I wanted to know what is the difference between these two approaches
1st
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthorization();
}
2nd
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
}
AddAuthorization() adds the bits necessary to use the Authorize attribute and policies. It does not apply authorization anywhere, that's left to you. Your description of this globally configuring the app is not correct.
Adding the authorize filter is basically applying the authorize attribute everywhere and requiring authorization over your entire site.

How to add token validation only for protected actions in ASP.NET 5 (ASP.NET Core)

I have added a JWT middleware to my application:
app.UseJwtBearerAuthentication(options => { options.AutomaticAuthenticate = true;} )
Now if my token does not validate (e.g. expired), I still get an error that lifetime validation did not pass. Is there a way to make the middleware validate the token only for protected resources? And if not, then how and where should I call what middleware does myself (reading the token into HttpContext.User)?
P.S This is how I add protection:
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
And this is how I allow public access:
[HttpGet]
[AllowAnonymous]
public string Get(int id)
{
}
To clarify: without the token this will work, but if the token is not valid (e.g. expired) even the public resource won't be accessible and 500 will be thrown (due to some internal bug cause 401 should be there really).
First, you need to disable automatic authentication by setting AutomaticAuthentication to false in your JWT bearer options.
To ensure the JWT bearer middleware is called for specific actions, you can create your own authorization policy using AddAuthenticationSchemes:
public void ConfigureServices(IServiceCollection services) {
services.AddAuthorization(options => {
options.AddPolicy("API", policy => {
policy.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme);
policy.RequireAuthenticatedUser();
});
});
}
Then, decorate your controller actions with the Authorize attribute:
[Authorize(Policy = "API")]
[HttpGet("your-action")]
public IActionResult Action() {
...
}