How to implement role authorization with custom database? - asp.net-mvc

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()
{
}
}

Related

How to access the current user in a specific aspect

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...

Impersonate for one request (Asp.net MVC)

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

MVC4 : adding roles in database

I have a project called Authorization with CodeFirstRoleProvider class that inherits from default RoleProvider
public class CodeFirstRoleProvider : RoleProvider
{
public override void CreateRole(string roleName)
{
if(string.IsNullOrEmpty(roleName)) return;
using(var context = new SISContext())
{
var role = context.Roles.SingleOrDefault(rl => rl.RoleName == roleName);//Roles table exists in database
if(role == null)
{
var newRole = new Role
{
RoleId = Guid.NewGuid(),
RoleName = roleName
};
context.Roles.Add(newRole);
context.SaveChanges();
}
}
}
}
In my other project WebPortal I want to use above method in let's say following way
var _role = new CodeFirstRoleProvider();
_role.CreateRole("Admin");
_role.CreateRole("NonAdmin");
now where do I need to place this code in my webportal? so that these roles gets added to database for the first time when the application runs.
Suppose this program runs for first time and someone clicks the register button I want to have a dropdownlist with above roles mentioned. So, these roles need to be in database before register is clicked.
Once the roles are in database I don't need to worry for the accessing these values in future.
One way would be manually writing the value in database but I don't want to do that, since I have this function I can use.
What If I do something like this?
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
var _role = new CodeFirstRoleProvider();//remove after role is added to database
_role.CreateRole("Admiin");//remove after role is added to database
_role.CreateRole("NonAdmin");//remove after role is added to database
ViewBag.ReturnUrl = returnUrl;
return View();
}
and once the roles are added to database i remove those code that does role adding.
Take a look at this article on seeding the SimpleMembership database. Even if you are not using the SimpleRoleProvider the same principals should apply since you are using code-first.

Custom RoleProvider keeps caching my repository object

I've implemented a custom RoleProvider for a project. The RoleProvider works, but the repository I use to fetch the user roles is only populated after build. When I logOff, change the role of the user, logOn again, the user still holds the old roles.
public class CmsRoleProvider : RoleProvider
{
private EntityDB _db { get; set; }
public CmsRoleProvider()
{
_db = new EntityDB();
}
public override string[] GetRolesForUser(string username)
{
var user = _db.Users.Where(u => u.EmailAddress == username).SingleOrDefault();
var roles = user.UserRoles.Select(u => u.Role.Name).ToList<string>();
return roles.ToArray();
}
}
In the sample above, the user only get the correct roles after building the project. When I create the repository inside the GetRolesForUser function, it works fine.
Is there a caching problem going on? Who can help me with this.
ASP.NET creates only single instance of the RoleProvider. Because of that your context is also long lived. Better to have short lived contexts.
public class CmsRoleProvider : RoleProvider
{
private EntityDB GetContext()
{
return new EntityDB();
}
public override string[] GetRolesForUser(string username)
{
using(var db = GetContext())
{
var user = db.Users.Where(u => u.EmailAddress == username)
.SingleOrDefault();
var roles = user.UserRoles.Select(u => u.Role.Name).ToList<string>();
return roles.ToArray();
}
}
}
Problem with your approach is that context keeps track of the loaded users. When you ask for a user that is already tracked by the context the existing instance is returned. Hence the UserRoles associated with that is returned.
The problem is a context reference. When you create a reference of the context (EntityDB), out of the method that get the roles from your context, this references still the same, in other words, every data that you select will be the same because the select will be made in the context not in database (this is a way to the EF not be going all the time to the database). The change that you do (in the roles), was made in another context, so to get the right context you have to create a new instance of you context. Do it inside of the method using the keywork using:
using (var database = new EntityDB())
{
// Get your roles and return them
}

Configure authorized roles dynamically via a config file in MVC Application

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.

Resources