Being relatively new to ASP MVC, I'm unsure which would better suit my needs. I have built an intranet site using Windows authentication and I'm able to secure controllers and actions using the Active Directory roles, e.g.
[Authorize(Roles="Administrators")]
[Authorize(Users="DOMAIN\User")]
public ActionResult SecureArea()
{
ViewBag.Message = "This is a secure area.";
return View();
}
I need to define my own security roles independent of the AD roles. The desired functionality is that authenticated users are granted access to specific actions according to one or more roles associated with their profile in my application database e.g: "Manager", "User", "Guest", "Analyst", "Developer" etc.
How do I create a custom role provider and/or custom authorization attribute(s)?
My solution was to create a custom role provider. Here are the steps I took, in case anyone else needs help later:
Create your custom user and role classes
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Security.Models.Security
{
public class AppRole : IdentityRole
{
}
}
and
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Security.Models.Security
{
public class AppUser : IdentityUser
{
}
}
Set up your database context
using Microsoft.AspNet.Identity.EntityFramework;
using Security.Models.Security;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web;
namespace Security.Models.DAL
{
public class UserContext : IdentityDbContext<AppUser>
{
public UserContext() : base("UserContext")
{
Database.SetInitializer<UserContext>(new CreateDatabaseIfNotExists<UserContext>());
}
}
}
Create your role provider and implement the following methods
using Security.Models.DAL;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
namespace Security.Models.Security
{
public class AppRoleProvider : RoleProvider
{
public override string[] GetAllRoles()
{
using (var userContext = new UserContext())
{
return userContext.Roles.Select(r => r.Name).ToArray();
}
}
public override string[] GetRolesForUser(string username)
{
using (var userContext = new UserContext())
{
var user = userContext.Users.SingleOrDefault(u => u.UserName == username);
var userRoles = userContext.Roles.Select(r => r.Name);
if (user == null)
return new string[] { };
return user.Roles == null ? new string[] { } :
userRoles.ToArray();
}
}
public override bool IsUserInRole(string username, string roleName)
{
using (var userContext = new UserContext())
{
var user = userContext.Users.SingleOrDefault(u => u.UserName == username);
var userRoles = userContext.Roles.Select(r => r.Name);
if (user == null)
return false;
return user.Roles != null &&
userRoles.Any(r => r == roleName);
}
}
}
}
Edit your web.config to set up the database connection and role provider reference
<connectionStrings>
<add name="UserContext" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\UserContext.mdf;Initial Catalog=UserContext;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
and
<system.web>
...
<authentication mode="Windows" />
<roleManager enabled="true" defaultProvider="AppRoleProvider">
<providers>
<clear/>
<add name="AppRoleProvider" type="Security.Models.Security.AppRoleProvider" connectionStringName = "UserContext"/>
</providers>
...
</roleManager>
</system.web>
In package manager console, enable migrations
enable-migrations
In the newly created Configurations.cs set up the user/role stores and managers and configure the user manager validator to accept '\' characters
namespace Security.Migrations
{
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using Security.Models.Security;
using System;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<Security.Models.DAL.UserContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
ContextKey = "Security.Models.DAL.UserContext";
}
protected override void Seed(Security.Models.DAL.UserContext db)
{
// Set up the role store and the role manager
var roleStore = new RoleStore<AppRole>(db);
var roleManager = new RoleManager<AppRole>(roleStore);
// Set up the user store and the user mananger
var userStore = new UserStore<AppUser>(db);
var userManager = new UserManager<AppUser>(userStore);
// Ensure that the user manager is able to accept special characters for userNames (e.g. '\' in the 'DOMAIN\username')
userManager.UserValidator = new UserValidator<AppUser>(userManager) { AllowOnlyAlphanumericUserNames = false };
// Seed the database with the administrator role if it does not already exist
if (!db.Roles.Any(r => r.Name == "Administrator"))
{
var role = new AppRole { Name = "Administrator" };
roleManager.Create(role);
}
// Seed the database with the administrator user if it does not already exist
if (!db.Users.Any(u => u.UserName == @"DOMAIN\admin"))
{
var user = new AppUser { UserName = @"DOMAIN\admin" };
userManager.Create(user);
// Assign the administrator role to this user
userManager.AddToRole(user.Id, "Administrator");
}
}
}
}
In package manager console, ensure the database is created and seeded
update-database
Create a custom authorization attribute that will redirect to an access denied page on failure
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Security.Models.Security
{
public class AccessDeniedAuthorizationAttribute : AuthorizeAttribute
{
public override void OnAuthorization(AuthorizationContext filterContext)
{
base.OnAuthorization(filterContext);
if(filterContext.Result is HttpUnauthorizedResult)
{
filterContext.Result = new RedirectResult("~/Home/AccessDenied");
}
}
}
}
You're done! You can now create an access denied page (in this case ~/Home/AccessDenied) and apply the attribute to any action, e.g.
using Security.Models.Security;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Security.Controllers
{
public class HomeController : Controller
{
...
[AccessDeniedAuthorizationAttribute(Roles = "Administrator")]
public ActionResult SecureArea()
{
return View();
}
public ActionResult AccessDenied()
{
return View();
}
...
}
}
Hope this helps someone in the future. Good luck!