Search code examples
asp.netasp.net-mvc

User is in role "admin" but [Authorize(Roles="admin")] won't authenticate


I found a great answer on SO describing how to set up custom user roles, and I've done the same in my project. So in my Login service I have:

public ActionResult Login() {
  // password authentication stuff omitted here
  var roles = GetRoles(user.Type); // returns a string e.g. "admin,user"
  var authTicket = new FormsAuthenticationTicket(
                    1,
                    userName,
                    DateTime.Now,
                    DateTime.Now.AddMinutes(20), // expiry
                    false,
                    roles,
                    "/");
  var cookie = new HttpCookie(FormsAuthentication.FormsCookieName, 
    FormsAuthentication.Encrypt(authTicket));
  Response.Cookies.Add(cookie);
  return new XmlResult(xmlDoc); // don't worry so much about this - returns XML as ActionResult
}

And in Global.asax.cs, I have (copied verbatim from the other answer):

protected void Application_AuthenticateRequest(Object sender, EventArgs e) {
  var authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
  if (authCookie != null) {
    var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    var roles = authTicket.UserData.Split(new Char[] { ',' });
    var userPrincipal = new GenericPrincipal(new GenericIdentity(authTicket.Name), roles);
    Context.User = userPrincipal;
  }
}

Then, in my ServicesController class, I have:

[Authorize(Roles = "admin")]
//[Authorize]
public ActionResult DoAdminStuff() {
  ...
}

I login as a user with the "admin" role, and that works. Then I call /services/doadminstuff - and I get access denied, even though when I put a breakpoint in Global.asax.cs, I can see that my roles do include "admin". If I comment out the first Authorize attribute (with roles) and just use a plain vanilla Authorize, then I can access the service.

I must be missing something critical here - but where to start looking?


Solution

  • I would recommend you use a custom authorize attribute instead of Application_AuthenticateRequest:

    [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true, AllowMultiple = true)]
    public class CustomAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            string cookieName = FormsAuthentication.FormsCookieName;
    
            if (!filterContext.HttpContext.User.Identity.IsAuthenticated ||
                filterContext.HttpContext.Request.Cookies == null ||
                filterContext.HttpContext.Request.Cookies[cookieName] == null
            )
            {
                HandleUnauthorizedRequest(filterContext);
                return;
            }
    
            var authCookie = filterContext.HttpContext.Request.Cookies[cookieName];
            var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
            string[] roles = authTicket.UserData.Split(',');
    
            var userIdentity = new GenericIdentity(authTicket.Name);
            var userPrincipal = new GenericPrincipal(userIdentity, roles);
    
            filterContext.HttpContext.User = userPrincipal;
            base.OnAuthorization(filterContext);
        }
    }
    

    and then:

    [CustomAuthorize(Roles = "admin")]
    public ActionResult DoAdminStuff() 
    {
        ...
    }
    

    Also a very important thing is to ensure that when you login an authentication cookie is emitted because you return an XML file. Use FireBug to inspect whether the authentication cookie is properly sent when you try to access the url /services/doadminstuff.