Adding authorization in ASP.NET Core application - asp.net-core

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.

Related

ASP.NET Core enrich IIdentity with custom profile

I am using Azure AD to authorize and authenticate the users.
All users have a profile in the database.
I would like on login to always "merge" the Azure user with my database user.
This is the code I am using to setup authentication in my web api.
public static partial class ServiceCollectionExtensions
{
public static IServiceCollection AddBearerAuthentication(this IServiceCollection services,
OpenIdConnectOptions openIdConnectOptions)
{
#if DEBUG
IdentityModelEventSource.ShowPII = true;
#endif
services
.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", o =>
{
o.Authority = openIdConnectOptions.Authority;
o.TokenValidationParameters.ValidIssuer = openIdConnectOptions.ValidIssuer;
o.TokenValidationParameters.ValidAudiences = openIdConnectOptions.ValidAudiences;
});
return services;
}
}
Can someone point me in the right direction?
Right now I am loading the user in all of my controllers, not pretty at all.
Not sure what do you mean by "merge" the user. But if it's just some logic you want to run for every incoming http request, you could just add a custom middleware
app.Use(async (context, next) =>
{
var user = await context.RequestServices
.GetRequiredService<DatabaseContext>()
.Users
.Where(....)
.SingleOrDefaultAsync();
...
await next(context);
});
Alternatively, if you want to couple your code with the authentication process very much, you could use the callback from JwtBearerOptions
.AddJwtBearer("Bearer", o =>
{
...
o.Events.OnTokenValidated = async context =>
{
var user = await context.HttpContext
.RequestServices
.GetRequiredService....
...
};
}
But personally, I think both approaches are bad. Going to the DB to get the user's credentials with every request is bad for performance. Also, it kinda defies the whole point of the JWT, which was designed specifically to not do that. The token should already contain all the claims inside. If it doesn't, I would suggest reconfiguring azure AD, or switch to self-issued tokens.

Authorize for all Gets on webApi NetCore

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

IdentityServer 4: No storage mechanism for grants specified - use AddInMemoryStores

I am using Identity Server 4 , ASP.NET Core and trying to replace the IdentityServer developer in Production environment. But getting the following error:
No storage mechanism for grants specified. Use the 'AddInMemoryStores' extension method to register a development version.
So, I tried to implement the services as mentioned in this answer:
IProfileService
IResourceOwnerPasswordValidator
This is my ConfigureServices Method in Startup class:
services.AddMvc();
var identityBuilder = services.AddIdentityServer();
identityBuilder.AddInMemoryScopes(identitySrvConfig.GetScopes());
identityBuilder.AddInMemoryClients(identitySrvConfig.GetClients());
identityBuilder.AddProfileService<ProfileService>();
identityBuilder.Services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
Taking into consideration that in my case the interface signature is different:
public class ResourceOwnerPasswordValidator : IdentityServer4.Validation.IResourceOwnerPasswordValidator
{
public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
throw new NotImplementedException();
}
}
But I am still getting the same error, what is the problem?
If you are applying custom Identity i.e
services.AddIdentity<AppUser, UserRole>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<AbcDbContext>();
then in
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Comment
app.UseIdentityServer();
because we are using custom identity, not default
They were/are reworking those APIs. You should use AddInMemoryPersistedGrants

Why .Net Core has its own claim types?

Based OpenidConnect specification the standard types for role claim and name claim is role and name. However in .net core System.Security.Claims.ClaimsIdentity.NameClaimType is set to "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" and System.Security.Claims.ClaimsIdentity.RoleClaimType is set to "http://schemas.microsoft.com/ws/2008/06/identity/claims/role"
My issue here is with role.
My ASP.NET core application is using OpenIdConnect for authentication. After successful authentication the OpenIdConnect provider sends back the role as a part of claims collection with Cliam.Type is set to role which is correct as per the OpenId specs.
However since .Net Core has its own type for role, IsInRole() method always returns false. Because I think IsInRole() method uses microsoft's role type for comparison.
Why .net is using differ types for claims instead of using standard convention? and how do I solve IsInRole() issue
Update 1
Well I tried configuring claim types during startup but it didn't work.
startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
// some stuff here that is not related to Identity like building configuration
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddApplicationInsightsTelemetry(Configuration);
services.AddAuthorization();
services.AddMvc(config =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
config.Filters.Add(new AuthorizeFilter(policy));
});
// Add Kendo UI services to the services container
services.AddKendo();
// Transform Microsoft cliam types to my claim type
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.ClaimsIdentity.RoleClaimType = "role";
options.ClaimsIdentity.UserNameClaimType = "name";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime)
{
loggerFactory.AddSerilog();
appLifetime.ApplicationStopped.Register(Log.CloseAndFlush);
app.UseExceptionHandler("/Home/Error");
app.UseApplicationInsightsRequestTelemetry();
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseIdentityServer(Configuration["Identity:Authority"], Configuration["Identity:ClientId"], Configuration["Identity:PostLogoutRedirectUri"]);
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
// Configure Kendo UI
app.UseKendo(env);
}
}
UseIdentityServer extension method
public static void UseIdentityServer(this IApplicationBuilder app, string authority, string clientId, string postlogoutRedirectUri)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme,
LoginPath = IdentityConstant.CallbackPath,
AccessDeniedPath = new PathString(IdentityConstant.AccessDeniedPath),
CookieName = IdentityConstant.AuthenticationCookieName,
});
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap = new Dictionary<string, string>();
var connectOptions = new OpenIdConnectOptions()
{
AutomaticChallenge = true,
Authority = authority,
ClientId = clientId,
ResponseType = IdentityConstant.ResponseType,
AuthenticationScheme = IdentityConstant.OpenIdAuthenticationScheme,
SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme,
PostLogoutRedirectUri = postlogoutRedirectUri,
CallbackPath = IdentityConstant.CallbackPath,
Events = new OpenIdConnectEvents()
{
OnTokenValidated = async context =>
{
var userInfoClient = new UserInfoClient(context.Options.Authority + IdentityConstant.UserInfoEndpoint);
var response = await userInfoClient.GetAsync(context.ProtocolMessage.AccessToken);
var claims = response.Claims;
//We will create new identity to store only required claims.
var newIdentity = new ClaimsIdentity(context.Ticket.Principal.Identity.AuthenticationType);
// keep the id_token for logout
newIdentity.AddClaim(new Claim(IdentityConstant.IdTokenClaim, context.ProtocolMessage.IdToken));
// add userinfo claims
newIdentity.AddClaims(claims);
// overwrite existing authentication ticket
context.Ticket = new AuthenticationTicket(
new ClaimsPrincipal(newIdentity),
context.Ticket.Properties,
context.Ticket.AuthenticationScheme);
await Task.FromResult(0);
}
}
};
connectOptions.Scope.Add(IdentityConstant.OpenIdScope);
connectOptions.Scope.Add(IdentityConstant.ProfileScope);
connectOptions.Scope.Add("roles");
app.UseOpenIdConnectAuthentication(connectOptions);
}
Update 2
I use IdentityServer3 for authentication for all our applications. If the client application is developed using classic ASP.NET MVC then ASP.Net's JWT handler will transform incoming role claim type to http://schemas.microsoft.com/ws/2008/06/identity/claims/role (More details can be found here under Claims Transformation section)
However same is not true when client application is developed using ASP.NET Core. The .net core WILL NOT transform claimtypes to .Net claim type, and that is correct. However .Net Core internally uses .Net claim type to find is user's role claims.
That means I need to Transform .Net claim types to desired claim type, but not sure where?
What are the standard conventions? You're only thinking of it from the context of the OpenId Connect specification which is not the only identity standard out there. Microsoft have made it generic enough to support all identity systems.
The fault here seems to be in the OpenId Connect authentication implementation for not providing a ClaimsPrincipal that uses the correct claim type for role.
Having said that you can fix it by implementing your own ClaimsPrincipal and override the IsInRole() method to use the correct claim type.
Alternatively you might consider putting in a place some middleware to apply the appropriate role claims based on the OpenId claims coming back?
You can configure the claim types during application startup.
services.AddIdentity<ApplicationUser, IdentityRole>(options => {
options.ClaimsIdentity.RoleClaimType = "http://yourdesiredclaimtype";
options.ClaimsIdentity.UserNameClaimType = "http://yourdesiredclaimtype";
});
You can see the claim options on GitHub.

Re-challenge authenticated users in ASP.NET Core

I'm running into some issues with the authentication pipeline in ASP.NET Core. My scenario is that I want to issue a challenge to a user who is already authenticated using OpenID Connect and Azure AD. There are multiple scenarios where you'd want to do that, for example when requesting additional scopes in a AAD v2 endpoint scenario.
This works like a charm in ASP.NET MVC, but in ASP.NET Core MVC the user is being redirected to the Access Denied-page as configured in the cookie authentication middleware. (When the user is not logged in, issuing a challenge works as expected.)
After a couple of hours searching the web and trying different parameters for my middleware options, I'm beginning to suspect that either I'm missing something obvious, or this behavior is by design and I need to solve my requirement some other way. Anyone any ideas on this?
EDIT: the relevant parts of my Startup.cs look like this:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
services.AddAuthentication(
SharedOptions => SharedOptions.SignInScheme = CookieAuthenticationDefaults.AuthenticationScheme);
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// <snip...>
app.UseCookieAuthentication(new CookieAuthenticationOptions { AuthenticationScheme = CookieAuthenticationDefaults.AuthenticationScheme });
var options = new OpenIdConnectOptions
{
AuthenticationScheme = OpenIdConnectDefaults.AuthenticationScheme,
ClientId = ClientId,
Authority = Authority,
CallbackPath = Configuration["Authentication:AzureAd:CallbackPath"],
ResponseType = OpenIdConnectResponseType.CodeIdToken,
PostLogoutRedirectUri = "https://localhost:44374/",
TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = false
}
};
options.Scope.Add("email");
options.Scope.Add("offline_access");
app.UseOpenIdConnectAuthentication(options);
}
And the Action looks like this:
public void RefreshSession()
{
HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
}
I found a hint and the solution here: https://github.com/aspnet/Security/issues/912.
ChallengeBehavior.Unauthorized is the "key".
This post gives the current (november 2016 - ASPNet 1.0.1) workaround: https://joonasw.net/view/azure-ad-b2c-with-aspnet-core
You'll need a new ActionResult to be able to call the AuthauticationManager.ChallengeAsync with the ChallengeBehavior.Unauthorized behavior.
Once the issue https://github.com/aspnet/Mvc/issues/5187 will be sucessfully closed, this should be integrated.
I tested it and it worked perfectly well (my goal was simply to extend Google scopes on a per user basis).
Try to sign out:
public void RefreshSession()
{
HttpContext.Authentication.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
HttpContext.Authentication.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
HttpContext.Authentication.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = "/" });
}