So @thomas seems to have a nice answer, buts its more wrapping your requirement of using enums, taking that into Roles that IPricipal
will understand. My solution is from bottom up, so you can use thomas' solution on top of mine to implement IPrincipal
I really needed something similar to what you want and was always scared with Forms Authentication, (yes you're scared too and I know it, but hear me out) So I always rolled out my own cheap authentication with forms, but a lot of things changed while I was learning mvc (in the last couple weeks) Forms auth is very dis separate and its very flexible. Essentially you're not really using forms auth, but your just plugging your own logic into the system.
So here is how I tackled this, (beware I am a learner myself).
Summary:
- You're going to override some of the forms auth classes to authenticate you own users, (you can even mock this)
- You're then going to create an
IIdentity
.
- You load up
GenericPrincipal
with a list of roles in strings (I know, no magic strings...keep reading)
Once you do the above, MVC understands enough to give you what you want! You can now use [Authorize(Roles = "Write,Read")]
over any controller and MVC will do almost everything. Now for no magic strings, all you have to do it create a wrapper around that attribute.
Long answer
You use the Internet Application Template that comes with MVC, So first you begin by creating MVC project, in the new dialog, say you want an Internet Application.
When you check the application, it will have one main class that overrides forms authentication. IMembershipService
Remove the local MembershipProvider variable __provider_ and in this class you should atleast add logic into the ValidateUser
Method. (Try adding a fake authentication to one user/pass) Also see the default v test application created in VS.
Implement IIdentity
public class MyIdentity : IIdentity
{
public MyIdentity(string username)
{
_username = username;//auth from the DB here.
//load up the Roles from db or whatever
}
string _username;
public User UserData { get; set; }
#region IIdentity Members
public string AuthenticationType
{
get { return "MyOwn.Authentication"; }
}
public bool IsAuthenticated
{
get { return true; }
}
public string Name
{
get { return _username; }
}
#endregion
public string[] Roles
{
get
{
return //get a list of roles as strings from your Db or something.
}
}
}
Remember we're still using the Default Internet Application template that comes with an MVC project.
So now AccountController.LogOn() should look like this:
[HttpPost]
public virtual ActionResult LogOn(LogOnModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (MembershipService.ValidateUser(model.UserName, model.Password))
{
FormsService.SignIn(model.UserName, model.RememberMe);
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(model.UserName, model.RememberMe, 15);
string encTicket = FormsAuthentication.Encrypt(ticket);
this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
if (Url.IsLocalUrl(returnUrl))
{
return Redirect(returnUrl);
}
else
{
return RedirectToAction("Index", "Home");
}
}
else
{
ModelState.AddModelError("", "The user name or password provided is incorrect.");
}
}
So what you're doing is setting a forms ticket like a session and then we'll read from it on every request like this: Put this in Global.asax.cs
public override void Init()
{
this.PostAuthenticateRequest += new EventHandler(MvcApplication_PostAuthenticateRequest);
base.Init();
}
void MvcApplication_PostAuthenticateRequest(object sender, EventArgs e)
{
HttpCookie authCookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];
if (authCookie != null)
{
string encTicket = authCookie.Value;
if (!String.IsNullOrEmpty(encTicket))
{
FormsAuthenticationTicket ticket = FormsAuthentication.Decrypt(encTicket);
MyIdentity id = new MyIdentity(ticket.Name);
//HERE is where the magic happens!!
GenericPrincipal prin = new GenericPrincipal(id, id.Roles);
HttpContext.Current.User = prin;
}
}
}
I asked a question one how efficient and correct the above method was here.
Ok now you are almost done, you can decorate your controllers like this:
[Authorize(Roles="RoleA,RoleB")]
(more on the strings later)
Theres one small problem here, if you decorate your controller with AuthorizeAttribute
, and the logged user does not have a particular permission, instead of saying "access denied" by default the user will be re directed to the login page to login again. You fix this like this(I tweaked this from an SO answer):
public class RoleAuthorizeAttribute : AuthorizeAttribute
{
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
// Returns HTTP 401
// If user is not logged in prompt
if (!filterContext.HttpContext.User.Identity.IsAuthenticated)
{
base.HandleUnauthorizedRequest(filterContext);
}
// Otherwise deny access
else
{
filterContext.Result = new RedirectToRouteResult(@"Default", new RouteValueDictionary{
{"controller","Account"},
{"action","NotAuthorized"}
});
}
}
}
Now all you do is add another wrapper around the AuthorizeAttribute
to support strong types that will translate into the strings that Principal expects. See this article for more.
I plan to update my application to use strong types later, I'll update this answer then.
I hope it helped.