ASP.NET Core custom defined User Roles implementation - asp.net-core-mvc

I am trying to define a custom way of User Roles since my DB's structure for the User table is the following structure:
Role is a bool so if it's true the User is an Admin, else he's a normal User.
I know I need to declare the add.UseAuthorization() in Startup.cs. and I can add the Attribute [Roles="Administrator"] / [Roles="User"] inside the Controller but I am not sure how to define the role to be determined by my Role column from the User table.
I've been searching the internet, reading about Policies too but I don't think that's the right way to implement. Everything I've found online is about some sort Identity structure but doesn't make any sense on how to attach it to my Role column.
Hope, someone can help me out. Thanks!

If you are free to manipulate your DB, I would highly suggest using IdentityFramework, it's a powerful framework which can integrate in your own database.
But to answer your question specifically, there's two steps missing:
Pick an authentication scheme to login the user (e.g. Cookie-based, ...)
Once the user logs in, save the designed Role in a ClaimsPrincipal object. This way the [Authorize(Roles = "User")] declaration can pick this up.
Below you'll find a basic example using the default ASP.NET Core template in Visual Studio.
Add the Authentication middleware your ConfigureServices method, and configure it using a AuthenticationScheme. In this case I'm using Cookie authentication.
//in ConfigureServices, add both middlewares
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie();
//in the Configure() method, enable these middlewares
app.UseAuthentication();
app.UseCookiePolicy(new CookiePolicyOptions());
Now you're ready for action. Let's say you have an Action method in which you want to authenticate the user. This is where you want to transform your Role so it can be recognised by [Authorize]
Get the value you need from your database. You'd end up with a bool. Convert it to a Role Claim, and add that to a ClaimsIdentity.
bool roleFromDb = true; //this comes from db
//convert to Claim of "Role" type, and create a ClaimsIdentity with it
var adminClaim = new Claim(ClaimTypes.Role, roleFromDb ? "Administrator" : "User");
var claimIdentity = new ClaimsIdentity(new[] { adminClaim },
CookieAuthenticationDefaults.AuthenticationScheme);
//signs in the user and add the ClaimsIdentity which states that user is Admin
await HttpContext.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimIdentity));
With that done, you can mark other actionmethods using the [Authorize] attribute, e.g:
[Authorize(Roles = "User")]
public IActionResult About() { ... }
[Authorize(Roles = "Administrator")]
public IActionResult Contact() { ... }
Now, only a signed in user with the "Administrator" role can visit the Contact page.
Check this resource for a more finetuned configuration of the middleware used.

Another way of implementing, based on my database without any modifications, is to use Claims and Cookies. I've managed doing this reading the following documents
Link One
Link Two
I've encountered only one major issue which was solved by reading this.
I'll also add the Login method and the Startup.cs rows, so others can see how to use it (if the docs aren't enough).
Login method from the Controller
[AllowAnonymous]
[HttpPost]
public async Task<IActionResult> Login(UserModel userModel)
{
if (_iUserBus.LoginUser(userModel))
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, userModel.Email),
new Claim(ClaimTypes.Role, _iUserBus.GetRole(userModel.Email)),
};
ClaimsIdentity userIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal principal = new ClaimsPrincipal(userIdentity);
var authProperties = new AuthenticationProperties
{
IsPersistent = false,
};
await HttpContext.SignInAsync(principal, authProperties);
return RedirectToAction("Index", "Home");
}
else
{
ModelState.AddModelError("Password", "Email and/or Password wrong");
return View();
}
}
Startup.cs
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
});
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.LoginPath = "/Users/Login";
options.LogoutPath = "/Users/Logout";
});
Hope this is useful to anyone in need.

Related

Identify the function name from policy middleware in .net core

I want to develop dynamic roles authorization using .net core webAPI, my structure is that user have one role and the role have some function or features to access
my question is there is any way yo get the function name where authorization policies applied
as example I have the following code
[Authorize(Roles = "Admin", Policy = "isHasPermission")]
public async Task<IActionResult> GetAllAsync()
{
var users = await _userService.GetAllAsync();
var userDtos = _mapper.Map<IList<UserDto>>(users);
return Ok(DataMessage.Data(new { users = userDtos }));
//return Ok(userDtos);
}
and my policy is something like that
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context,
isHasPermissionRequirement requirement)
{
/*
CAN I GET THE FUNCTION NAME "GetAllAsync" HERE!
TO VALIDATE IF IT IS ONE OF USER'S FEATURE
*/
return await Task.CompletedTask;
}
So that I need to get the function name in the policy to validate user's permissions, if it is possible or not?
You are doing it backwards: The way policies work is that you say that a certain action has requirements. It is not a valid requirement to then circle back to where the policy is used. Policies should be completely separate from what you are trying to access. If a certain thing specifies a policy, then just the presense of the policy should be all that’s necessary.
If you want to have your logic actually check what you are trying to access, then you could look into authorization filters instead. When they are called, they pass an AuthorizationFilterContext which also contains information about the route and action the user is trying to access. With that, you can get the action name for example using (context.ActionDescriptor as ControllerActionDescriptor).ActionName.

How to do Role-based Web API Authorization using Identity Server 4 (JWT)

This is all new to me and I'm still trying to wrap my head around it. I've got an IDP (Identity Server 4) set up, and I was able to configure a client to authenticate to it (Angular 6 App), and further more to authenticate to an API (Asp.Net Core 2.0). It all seems to work fine.
Here's the client definition in the IDP:
new Client
{
ClientId = "ZooClient",
ClientName = "Zoo Client",
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
RequireConsent = true,
RedirectUris = { "http://localhost:4200/home" },
PostLogoutRedirectUris = { "http://localhost:4200/home" },
AllowedCorsOrigins = { "http://localhost:4200" },
AllowedScopes =
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
IdentityServerConstants.StandardScopes.Phone,
IdentityServerConstants.StandardScopes.Address,
"roles",
"ZooWebAPI"
}
}
I'm requesting the following scopes in the client:
'openid profile email roles ZooWebAPI'
The WebAPI is set up as such:
public void ConfigureServices(IServiceCollection services)
{
services
.AddMvcCore()
.AddJsonFormatters()
.AddAuthorization();
services.AddCors();
services.AddDistributedMemoryCache();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "https://localhost:44317";
options.RequireHttpsMetadata = false;
options.ApiName = "ZooWebAPI";
});
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseCors(policy =>
{
policy.WithOrigins("http://localhost:4200");
policy.AllowAnyHeader();
policy.AllowAnyMethod();
policy.AllowCredentials();
policy.WithExposedHeaders("WWW-Authenticate");
});
app.UseAuthentication();
app.UseMvc();
}
By using [Authorize] I was successfully able to secure the API:
[Route("api/[controller]")]
[Authorize]
public class ValuesController : Controller
{
// GET api/values
[HttpGet]
public ActionResult Get()
{
return new JsonResult(User.Claims.Select(
c => new { c.Type, c.Value }));
}
}
Everything works fine, if client is not authenticated, browser goes to IDP, requires authentication, redirects back with access token, access token is then used for API calls that are successfully made.
If I look at the Claims in the User object, I can see some information, but I don't have any user information. I can see the scopes, and etc, but no roles for example. From what I read, that is to be expected, and the API should not care about what user is calling it, but how would I go by restricting API calls based on roles? Or would that be completely against specs?
The IDP has an userinfo end point that returns all the user information, and I thought that would be used in the WebAPI, but again, from some reading, it looks like the intention is for that end point to be called from the client only.
Anyway, I would like to restrict Web API calls based on the roles for a specific user. Does anyone have any suggestions, comments? Also, I would like to know what user is making the call, how would I go by doing that?
JWT example:
Thanks
From what I can learn from your information, I can tell the following.
You are logging in through an external provider: Windows Authentication.
You are defining some scopes to pass something to the token that indicates access to specific resources.
The User object you speak of, is the User class that gets filled in from the access token. Since the access token by default doesn't include user profile claims, you don't have them on the User object. This is different from using Windows Authentication directly where the username is provided on the User Principle.
You need to take additional action to provide authorization based on the user logging in.
There a couple of points where you can add authorization logic:
You could define claims on the custom scopes you define in the configuration of Identityserver. This is not desirable IMHO because it's fixed to the login method and not the user logging in.
You could use ClaimsTransformation ( see links below). This allows you to add claims to the list of claims availible at the start of your methods. This has the drawback ( for some people an positive) that those extra claims are not added to the access token itself, it's only on your back-end where the token is evaluated that these claims will be added before the request is handled by your code.
How you retrieve those claims is up to your bussiness requirements.
If you need to have the user information, you have to call the userinfo endpoint of Identityserver to know the username at least. That is what that endpoint is intended for. Based on that you can then use your own logic to determine the 'Roles' this user has.
For instance we created an separate service that can configure and return 'Roles' claims based upon the user and the scopes included in the accesstoken.
UseClaimsTransformation .NET Core
UseClaimsTransformation .NET Full framework

Passing TempData with RedirectToAction

Intro:
I am a .NET studet trying to learn ASP.NET Core MVC. So please be understanding. I have searched the web for an answer to my problem, but havent found a solution that works for me.
Problem:
I want to pass a validation message from my create post method to the index IActionmethod whenever a post has been created and them show it as an alert message for now. I have read on the web that ViewBag dosent survive a redirect, but a TempData does. This is my code so far.
Create post method:
public IActionResult CreatePost(string textContent, string headline, string type)
{
var catType = new Category() { CategoryType = type.ToUpper() };
if (db.Category.Any(s => s.CategoryType.Trim().ToLower() == type.Trim().ToLower()))
catType = db.Category.FirstOrDefault(s => s.CategoryType.Trim().ToLower() == type.Trim().ToLower());
var newPost = new Post()
{
Content = textContent,
Header = headline,
DateOfPost = DateTime.Now,
category = catType
};
db.Posts.Add(newPost);
db.SaveChanges();
TempData["validation"] = "Your post hase been publsihed";
return RedirectToAction("Index");
}
The index method:
public IActionResult Index()
{
var validation = TempData["validation"];
var posts = (from x in db.Posts
orderby x.DateOfPost descending
orderby x.PostID descending
select x);
return View(posts);
}
I have tried this guide: ClickThis and this one: ClickThis2 but I got this message:
I know this line from gudie number 2 might be important, but didnt now how to apply it. -
var product = TempData["myTempData"] as Product;
The last thing I want to do is pass it to the index view, but dont know how. I am currently passing a model from the index.
Tell me if it is anything more you would like to see. Like dependencies.
All the help I get is gold and will be much appreciate!!!
I landed on this question while googling for "asp.net core redirect to action tempdata". I found the answer and am posting it here for posterity.
Problem
My issue was that, after filling in some TempData values and calling RedirectToAction(), TempData would be empty on the page that I was redirecting to.
Solution
Per HamedH's answer here:
If you are running ASP.NET Core 2.1, open your Startup.cs file and make sure that in your Configure() method app.UseCookiePolicy(); comes after app.UseMVC();.
Example:
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
...
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
app.UseCookiePolicy();
}
Did you configure Session? TempData is using session behind the scenes.
Project.json
"Microsoft.AspNetCore.Session": "1.1.0"
Here is the Startup.cs file. - ConfigureServices method
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache();
services.AddSession();
services.AddMvc();
}
And Configure method.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseSession();
app.UseMvc(routes => {
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
Now try with TempData, it will work.
And you can set the environment with set ASPNETCORE_ENVIRONMENT=Development environment variable.
TempData stores data server-side, under user Session. You need to enable sessions (as exception message says). Check this manual.
If you don't want to use sessions - you need some other way to store data (cookies?)
Providers
The TempData is using various providers for storing the state. By default the cookie based data provider is used.
Session is just an alternative
If your application do not use session I do not see any reason to use it only for TempData store.
Cookie Consent
ASP NET Core 2.1 have some new GDPR features based on cookies. By default, data should be stored in cookies only with the user's consent. If the user does not agree with the storing data in cookies, TempData cannot work. This behavior varies across versions of ASP NET Core.
If you do not want to hold any sensitive data in cookies, you can obviously change the settings.
app.UseCookiePolicy(new CookiePolicyOptions
{
CheckConsentNeeded = context => false
});
You can set the CookiePolicyOptions separatelly in ConfigureServices as well. It is a quite cleaner.
Story continues
We have two kind of data in the cookies. Essential data (needed for running application) and non-essential (some user data). User consent is needed for non-essential data. TempData is non-essential. You can set you TempData as essential and user consent is not needed anymore:
services.Configure<CookieTempDataProviderOptions>(options => {
options.Cookie.IsEssential = true;
});
I highly recommend to think about this before copy / paste.
I'm just posting this for anyone who comes across this problem in an ASP.NET MVC application, #Ahmar's answer made me go look at my logout method, I was using Session.Abandon() before redirecting to the login page.
I just changed it to Session.Clear() to reset the session instead of removing it completely and now the TempData is working in the method I'm redirecting to.

Simple login to session

I work on simple project in .net core. It school task, so I do not need any advanced practices. Can you tell me what is the simplest way to set default view when session is null ? For example When user manually enter Url /Home/Tasks he will be redirected to Account/Login until enter correct login. Thanks
You achieve that simply use basic authentication. Choose Individual User Accounts option while creating new application:
After that take a look at the Startup.cs class and add following lines to ConfigureServices method:
public void ConfigureServices(IServiceCollection services)
{
services.Configure<IdentityOptions>(options =>
{
options.Cookies.ApplicationCookie.LoginPath = new PathString("/Login");
options.Cookies.ApplicationCookie.LogoutPath = new PathString("/Logoff");
});
}
or
services.Configure<CookieAuthenticationOptions>(options =>
{
options.LoginPath = new PathString("/Account/Login");
});
Ones this is done you can mark your controller with an [Authorize] attribute and all actions of that controller will require user to be logged in:
[Authorize]
public class HomeController : Controller
{
...
}

How to tell if ASP.NET impersonation is working?

With ASP.NET impersonation, can one use Environment.UserName to determine if impersonation is working? That is if the site is impersonating properly, should Environment.UserName return my username?
You should use User.Identity.Name:
[Authorize]
public ActionResult Foo()
{
// If we got so far it means that the user is authorized to
// execute this action according to our configuration =>
// we can work with his username
string username = User.Identity.Name;
...
}

Resources