I current have the following attribute decorating one of the action method.
[Authorize(Roles = "Admin")]
public ActionResult DoAdminTask()
{
//Do something
return View();
}
Currently, only users in the Admin role can invoke this method, but this will change. Is there anyway I can store a list of authorised roles in a config file, rather than hard coding it into the source?
EDIT: Roles will change over time, and more than 1 role will need access.
i.e. Users in either role A OR role B can access.
No way to do this with the standard authorize attribute, but you could extend the authorize attribute with your own custom authorize attribute and have it use a configuration file to determine the mapping between controller/action and the set of roles.
but you can use something like
public static class AppRoles
{
public const string Users = "UsersRoleName";
public const string Admin = "AdminRoleName";
}
and then Controller can have authorize attribute as
[Authorize(Roles = AppRoles.Admin)]
I felt this question deserved an answer with a code sample... Taking #tvanfosson's suggestion of extending the AuthorizeAttribute class, here's what I came up with (criticism is more than welcome).
AuthorizeFromConfiguration.cs:
public class AuthorizeFromConfiguration: AuthorizeAttribute
{
public new string Roles
{
get {
return base.Roles;
}
set {
var config = new ConfigurationBuilder()
.SetBasePath(Environment.CurrentDirectory)
.AddJsonFile("authorization.json")
.Build();
base.Roles = config[value];
}
}
}
authorization.json:
{
"Parts": {
"Create": "contoso.com\\MyWebApp_CreateNewPart",
"Edit": "contoso.com\\MyWebApp_EditPart"
}
}
Example Usage:
[AuthorizeFromConfiguration(Roles = "Parts:Create")]
public class CreateModel : PageModel
{
//...
}
Note: In my testing, the web-site had to be restarted before any changes to authorization.json file took effect, even when I tried changing the logic so that the JSON file was read on the get accessor instead of the set.
Related
I have an application which requires role authorization using custom database. The database is set up with a tblUsers table that has a reference to a tblRoles table. The users are also already assigned to their roles.
I also want to use the [Authorize(Role = "RoleName")] attribute on each action to check if an authenticated user is assigned to "RoleName" in the database. I'm having a lot of trouble figuring out where I need to make a modification to the [Authorize] attribute so it behaves that way. I just want to see if a username has a role, I won't have a page to manage roles in the database.
I have tried implementing custom storage providers for ASP.NET Core Identity, but it's starting to look like this is not what I need because I'm not gonna be managing roles within the application, and I can't tell how it affects the behavior of [Authorize] attribute.
Also, it's likely that I have a false assumption in my understanding on how the [Authorize] attribute even works. If you notice it, I would appreciate if you could point it out.
I had a similar problem when my client asked for granular permissions for each role. I couldn't find a way to modify the Authorize attribute but was able to implement the solution with a custom attribute. But it depends on one thing i.e can you get the userId of the calling user? I used cookie authentication so I just include the userId in my claims when someone logs in so when a request comes I can always get it from there. I think the built-in Session logic in asp.net might get the job done too, I can't say for sure though. Anyways the logic for custom authorization goes like this:
Load users and roles from database to cache on startup. If you haven't set up a cache in your program (and don't want to) you can simply make your own for this purpose by making a UserRoleCache class with 2 static lists in it. Also there are several ways of loading data from db on startup but I found it easy to do that directly in Program.cs as you'll see below.
Define your custom attribute to check if the calling user has the required role by iterating over lists in cache and return 403 if not.
Modify your Program class like:
public class Program
{
public static async Task Main(string[] args)
{
IWebHost webHost = CreateWebHostBuilder(args).Build();
using (var scope = webHost.Services.CreateScope())
{
//Get the DbContext instance. Replace MyDbContext with the
//actual name of the context in your program
var context = scope.ServiceProvider.GetRequiredService<MyDbContext>();
List<User> users = await context.User.ToListAsync();
List<Role> roles = await context.Role.ToListAsync();
//You may make getters and setters, this is just to give you an idea
UserRoleCache.users = users;
UserRoleCache.roles = roles;
}
webHost.Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args)
.UseStartup<Startup>();
}
Then comes the logic for checking if user has a role. Notice I've used an array of roles because sometimes you'll want to allow access to multiple roles.
public class RoleRequirementFilter : IAuthorizationFilter
{
private readonly string[] _roles;
public PermissionRequirementFilter(string[] roles)
{
_roles = roles;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
bool hasRole = false;
//Assuming there's a way you can get the userId
var userId = GetUserId();
User user = UserRoleCache.users.FirstOrDefault(x => x.Id == userId);
//Where roleType is the name of the role like Admin, Manager etc
List<Role> roles = UserRoleCache.roles.FindAll(x => _roles.Contains(x.RoleType))
foreach(var role in roles)
{
if(user.RoleId == role.Id)
{
hasRole = true;
break;
}
}
if (!hasRole)
context.Result = new StatusCodeResult(403);
}
}
Finally make the Role attribute
public class RoleAttribute : TypeFilterAttribute
{
public RoleAttribute(params string[] roles) : base(typeof(RoleRequirementFilter))
{
Arguments = new object[] { roles };
}
}
Now you can use the Role attribute in your controllers:
public class SampleController : ControllerBase
{
[HttpGet]
[Role("Admin", "Manager")]
public async Task<ActionResult> Get()
{
}
[HttpPost]
[Role("Admin")]
public async Task<ActionResult> Post()
{
}
}
If I adopted the last scenario in this thesis :
Then my main layers will be like that:
UI Service (MVC application)
Business Layer
Security Service (used as a wrapper class library for MS identity
framework)
Aspects which use the previous security service to Authorize the
business layer methods.
public class EditEmployeeData : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Employee emp = (Employee)args.Instance;
((System.Security.Claims.ClaimsIdentity)System.Web.HttpContext.Current.User.Identity).HasClaim("Employee", "EditName");
}
}
I want to set the current user in runtime.
How to access the current user to authorize him on a specific
functionality in business layer?
Should the authorization be more near to the UI to disable/hide functionality and to prevent calling not allowed action methods ?(In the preferred scenario there's not any interaction between the security layer and the UI !!)
Update
Please see this answer about using claims...
In a controller, you can get the current user like this:
using Microsoft.AspNet.Identity.Owin;
public class MyController : Controller
{
// this code will return 0 if user is not authenticated
protected long GetUserId()
{
// note: I have changed the default UserId type from Guid to long
return User.Identity.GetUserId<long>();
/*
* use this if you are using Guid UserIds (which is the default)
* return User.Identity.GetUserId();
*/
}
See this, if you want to know how to change type of UserId.
If you have access to HttpContext, you can get the user like this:
// note that I have changed UserId from Guid to long
HttpContext.Current.User.Identity.GetUserId<long>()
If you want to get ApplicationUser use this (more info here):
// this is how you get user manager from OwinContext
var userManager = System.Web.HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>();
// Get ApplicationUser from UserManager
ApplicationUser user = UserManager.FindById(User.Identity.GetUserId());
How to access the current user to authorize him on a specific
functionality in business layer?
If you need to access current user in a service, you can pass it through or you can inject it. Using ninject, this is how you can inject UserId into a service:
kernel.Bind<MyService>().ToConstructor(ctorArg => new MyService(
HttpContext.Current.User.Identity.GetUserId<long>()).InRequestScope();
And this is how MyService class looks like:
public class MyService
{
private readonly long _userId;
public MyService(long userId)
{
// this service always has access to current user (if logged in)
_userId = userId;
}
// more code...
I am not sure what is the process of your authorization... ASP.NET Identity, already implements authorization task for you. This is implemented in ApplicationUserManager and ApplicationSignInManager which comes with ASP.NET MVC default template. You can use [Authorize] attribute on your action/class to prevent unauthorized access:
[Authorize] // <-- restricts all action methods of the class, unless marked [AllowAnonymous]
public class MyController : Controller
{
[HttpPost]
[Authorize] // <-- restricts this particular action method
public ActionResult MyAction(long id)
{
// do some action which requires authorization
}
Regarding DDD layers, have a look at this this link which explains services which belong to each layer.
How to access the current user to authorize him on a specific functionality in business layer?
To access user information on the business layer, you can type an interface named ICurrentUser
namespace AOPSample
{
public interface ICurrentUser
{
User GetCurrentUser();
}
public class User
{
public int Id { get; set; }
public string Username { get; set; }
public string Role { get; set; }
}
}
The CurrentUser class must be able to read the information of the user from a common location. HttpContext is available for this.
Let's write a helper class for this.
using System.Web;
namespace AOPSample
{
public class ContextHelper
{
public T Get<T>()
{
T local = default(T);
string key = typeof(T).GUID.ToString();
if (HttpContext.Current.Items.Contains(key))
{
local = (T)HttpContext.Current.Items[key];
}
return local;
}
public T Get<T>(string key)
{
T local = default(T);
if (HttpContext.Current.Items.Contains(key))
{
local = (T)HttpContext.Current.Items[key];
}
return local;
}
public void Set<T>(T value)
{
string str = typeof(T).GUID.ToString();
HttpContext.Current.Items[str] = value;
}
public void Set<T>(T value, string key)
{
HttpContext.Current.Items[key] = value;
}
}
}
Our CurrentUser class will return user information using your helper class
namespace AOPSample
{
public class CurrentUser : ICurrentUser
{
public User GetCurrentUser()
{
return new ContextHelper().Get<User>();
}
}
}
now user information write to HttpContext with ContextHelper class and for this use correct location interceptor class
public class EditEmployeeData : OnMethodBoundaryAspect
{
public override void OnEntry(MethodExecutionArgs args)
{
Employee emp = (Employee)args.Instance;
((System.Security.Claims.ClaimsIdentity)System.Web.HttpContext.Current.User.Identity).HasClaim("Employee", "EditName");
new ContextHelper().Set<User>(new User
{
});
}
}
You can access user information from the domain layer with ICurrentUser. HttpContext is unique for every request and response
Should the authorization be more near to the UI to disable/hide functionality and to prevent calling not allowed action methods ?(In the preferred scenario there's not any interaction between the security layer and the UI !!)
It's your choice
In my opinion, you can take user privileges and log them with cache and use them for client side actions, but according to the technology you use for server side, you can store user information for each request in a similar way. For example; The correct location to store the OperationContext for wcf.
If you use ASP.NET Identity, you can try the following approach in order to get current User:
ApplicationUser user = System.Web.HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>().FindById(System.Web.HttpContext.Current.User.Identity.GetUserId());
//If you use int instead of string for primary key, use this:
ApplicationUser user = System.Web.HttpContext.Current.GetOwinContext().GetUserManager<ApplicationUserManager>().FindById(Convert.ToInt32(System.Web.HttpContext.Current.User.Identity.GetUserId()));
Hope this helps...
In my ASP.net MVC project I've got (among other roles) moderators and users. I want to give the moderators the option to "see current page as user".
My approach is to create a ActionFilterAttribute and overload OnActionExecuting & OnResultExecuted as the page is then rendered for the given user.
The first idea there was to juggle with the Roles:
OnActionExecuting {
... //various checks, if role exist, if user want to switch
var tempRoles = Roles.getRolesForUser(user);
filterContext.HttpContext.Items["tempRole"] = tempRoles;
Roles.RemoveUserFromRoles(user, tempRoles)
Roles.AddUserToRole(user, targetRole);
}
and then
OnResultExecuted {
//if switched view
{
Roles.RemoveUserFromRole(user,targetRole)
Roles.AddUserToRoles(filterContext.HttpContext.Items["tempRole"])
}
This works, but in a worst case scenario the roles are gone, so i prefer to not touch them...
My second idea was to create a dummy user add him to the userroles sign the moderator into this account with FormsAuthentication.SetAuthCookie(dummyUser, true) and revert everything in the OnResultExecuted, so in a worst case scenario the user is in the dummyRole (where he can logout) and the dummyUser is in the Database.
After debugging and researching I realised that SetAuthCookie requires a Redirect to come into effect - so it doesn't work this way.
The questions:
Is there a way to force SetAuthCookie to come into affect without a redirect
Any other suggestion/approaches how to accomplish this "see page as other user"?
If my first idea is the only solution, how do i make it foolproof?
Ahoi Christian,
you could decorate the class SqlRoleProvider and add it to the role manager.
See Sample Role-Provider Implementation:
http://msdn.microsoft.com/en-us/library/tksy7hd7%28v=vs.100%29.aspx
The decorated SqlRoleProvider could overwrite the IsUserInRole method and thereby implement impersonation functionality.
edit: I have added the code below:
public class MyRoleProvider : SqlRoleProvider
{
private static ConcurrentDictionary<string, string> impersonationList;
public MyRoleProvider() : base()
{
impersonationList = new ConcurrentDictionary<string, string>();
}
public static void startImpersonate(string username, string rolename)
{
impersonationList.TryAdd(username,rolename);
}
public override string[] GetRolesForUser(string username) {
if (impersonationList.ContainsKey(username))
return new string[] { impersonationList[username] };
else
return base.GetRolesForUser(username);
}
public static void stopImpersonate(string username)
{
string rolename;
impersonationList.TryRemove(username, out rolename);
}
}
If I have the Authorize attribute on both the controller and the action, which one will take the effect? Or will both take effect?
You asked:
If I have Authorize attribute on both controller and action, which one will take the effect? Both?
To answer this simply: Both. The effect is to AND the two restrictions together. I'll explain why below ...
Details
So, there are a few reasons you could be asking this.
You want to know how to enforce an additional constraint on an Action compared to a method. e.g.
At controller level, enforce users in the role "user"
At an action level, additionally enforce users in the role "admin"
You want to replace the controller constraint at the action level
You want to remove the controller constraint at the action level and make the method available to anonymous users
You didn't specify your MVC version, so I will assume the latest as of today (MVC 4.5). However, that won't change of the answer much even if you were using MVC 3.
[Anonymous] overrides controller [Authorize] (case 3)
Case 3. I don't need to cover (the use of [AllowAnonymous]) as it has been answered all over SO and all over the web already. Suffice to say: if you specify [AllowAnonymous] on an action it will make that action public even if the controller has [Authorize] on it.
You can also make an entire website subject to authorisation by using a global filter, and use AllowAnonymous on the few actions or controllers you want to make public.
[Authorize] is additive (case 1)
Case 1 is easy. Take the following controller as an example:
[Authorize(Roles="user")]
public class HomeController : Controller {
public ActionResult AllUsersIndex() {
return View();
}
[Authorize(Roles = "admin")]
public ActionResult AdminUsersIndex() {
return View();
}
}
By default [Authorize(Roles="user")] makes all Actions in the Controller available to accounts in the "user" role only. Therefore to access AllUsersIndex you must be in the "user" role. However to access AdminUsersIndex you must be both in the "user" and the "admin" role. For example:
UserName: Bob, Roles: user, cannot access AdminUsersIndex, but can access AllUsersIndex
UserName: Jane, Roles: admin, cannot access AdminUsersIndex or AllUsersIndex
UserName: Tim, Roles: user & admin, can access AdminUsersIndex and AllUsersIndex
This illustrates that the [Authorize] attribute is additive. This is also true of the Users property of the attribute, which can be combined with Roles to make it even more restrictive.
This behaviour is due to the way that controller and action attributes work. The attributes are chained together and applied in the order controller then action. If the first one refuses authorization, then control returns and the action's attribute is not called. If the first one passes authorization, then the second one is then checked as well. You can override this order by specifying Order (for example [Authorize(Roles = "user", Order = 2)]).
Overriding [Authorize] (case 2)
Case 2 is trickier. Recall from above that the [Authorize] attributes are examined in the order (Global then) Controller then Action. The first one to detect that the user is ineligible to be authorized wins, the others don't get called.
One way around this is to define two new attributes as below. The [OverrideAuthorize] does nothing other than defer to [Authorize]; its only purpose is to define a type that we can check for. The [DefaultAuthorize] allows us to check to see if the Action being called in the request is decorated with a [OverrideAuthorize]. If it is then we defer to the Action authorization check, otherwise we proceed with the Controller level check.
public class DefaultAuthorizeAttribute : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext)
{
var action = filterContext.ActionDescriptor;
if (action.IsDefined(typeof(OverrideAuthorizeAttribute), true)) return;
base.OnAuthorization(filterContext);
}
}
public class OverrideAuthorizeAttribute : AuthorizeAttribute {
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
}
}
We can then use it like this:
[DefaultAuthorize(Roles="user")]
public class HomeController : Controller {
// Available to accounts in the "user" role
public ActionResult AllUsersIndex() {
return View();
}
// Available only to accounts both in the "user" and "admin" role
[Authorize(Roles = "admin")]
public ActionResult AdminUsersIndex() {
return View();
}
// Available to accounts in the "superuser" role even if not in "user" role
[OverrideAuthorize(Roles = "superuser")]
public ActionResult SuperusersIndex() {
return View();
}
}
In the above example SuperusersIndex is available to an account that has the "superuser" role, even if it does not have the "user" role.
I would like to add something to Overriding [Authorize] (case 2)
OverrideAuthorizeAttribute and DefaultAuthorizeAttribute works fine, but I discover that you can also use
OverrideAuthorizationAttribute which overrides authorization filters defined at a higher level.
[Authorize(Roles="user")]
public class HomeController : Controller {
// Available to accounts in the "user" role
public ActionResult AllUsersIndex() {
return View();
}
// Available only to accounts both in the "user" and "admin" role
[Authorize(Roles = "admin")]
public ActionResult AdminUsersIndex() {
return View();
}
// Available to accounts in the "superuser" role even if not in "user" role
[OverrideAuthorization()]
[Authorize(Roles = "superuser")]
public ActionResult SuperusersIndex() {
return View();
}
}
If use it on controller then, all methods of this controller will effected.
[Authorize]
public class SomeController(){
// all actions are effected
public ActionResult Action1
public ActionResult Action2
If you want to prevent for one of these actions, you can use something like this:
[Authorize]
public class SomeController(){
// all actions are effected
public ActionResult Action1
public ActionResult Action2
[AllowAnonymous]
public ActionResult Action3 // only this method is not effected...
I made an adaptation of this answer's second case for ASP.NET Core 2.1.
The difference with ASP.NET Core's AuthorizeAttribute is that you don't have to call AuthorizeAttribute.OnAuthorization base method to proceed to normal authorization. This means that even if you don't explicitly call the base method, the base AuthorizeAttribute could still short-circuit authorization by forbidding access.
What I did is that I created a DefaultAuthorizeAttribute that does not inherit from AuthorizeAttribute, but from Attribute instead. Since the DefaultAuthorizeAttribute does not inherit from AuthorizeAttribute, I had to recreate the authorization behavior.
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public class DefaultAuthorizeAttribute : Attribute, IAuthorizationFilter
{
private readonly AuthorizeFilter m_authorizeFilter;
public DefaultAuthorizeAttribute(params string[] authenticationSchemes)
{
var policyBuilder = new AuthorizationPolicyBuilder()
.AddAuthenticationSchemes(authenticationSchemes)
.RequireAuthenticatedUser();
m_authorizeFilter = new AuthorizeFilter(policyBuilder.Build());
}
public void OnAuthorization(AuthorizationFilterContext filterContext)
{
if (filterContext.ActionDescriptor is ControllerActionDescriptor controllerAction
&& controllerAction.MethodInfo.GetCustomAttributes(typeof(OverrideAuthorizeAttribute), true).Any())
{
return;
}
m_authorizeFilter.OnAuthorizationAsync(filterContext).Wait();
}
}
public class OverrideAuthorizeAttribute : AuthorizeAttribute, IAuthorizationFilter
{
public void OnAuthorization(AuthorizationFilterContext filterContext) { }
}
I am building a simple CMS in which roles are set dynamically in the admin panel. The existing way of authorizing a controller method, adding [Authorize(Roles="admin")] for example, is therefore no longer sufficient. The role-action relationship must be stored in the database, so that end users can easily give/take permissions to/from others in the admin panel. How can I implement this?
If you want to take control of the authorization process, you should subclass AuthorizeAttribute and override the AuthorizeCore method. Then simply decorate your controllers with your CmsAuthorizeAttribute instead of the default.
public class CmsAuthorizeAttribute : AuthorizeAttribute
{
public override virtual bool AuthorizeCore(HttpContextBase httpContext)
{
IPrincipal user = httpContext.User;
IIdentity identity = user.Identity;
if (!identity.IsAuthenticated) {
return false;
}
bool isAuthorized = true;
// TODO: perform custom authorization against the CMS
return isAuthorized;
}
}
The downside to this is that you won't have access to ctor-injected IoC, so you'll have to request any dependencies from the container directly.
That is exactly what the ASP.NET membership / profile stuff does for you. And it works with the Authorize attribute.
If you want to roll your own you could create a custom action filter that mimics the behavior of the standard Authorize action filter does. Pseudo code below.
public MyAuthorizeAttribute : ActionFilterAttribute
{
public string MyRole { get; set; }
public void OnActionExecuting(ControllerContext context)
{
if (!(bool)Session["userIsAuthenticated"])
{
throw new AuthenticationException("Must log in.");
}
if (!Session["userRoles"].Contains(MyRole))
{
throw new AuthenticationException("Must have role " + MyRole);
}
}
}
The role - action relationship must be
stored in the database
You will have to check your security within the controller method, unless you want to subclass AuthorizeAttribute so that it looks up the roles from the database for you.