broadband
broadband

Reputation: 3498

Custom ASP.NET MVC Forms Authentication

I want to do custom authentication because we have many controllers and it makes sense to create global filter that applies for all controllers and their actions with exception of login page.

In Global.asax.cs I added next global filter:

  public class Global : HttpApplication
 {
   void Application_Start(object sender, EventArgs e) // Code that runs on application startup
  {
    ... // only showing important part
    GlobalFilters.Filters.Add(new Filters.AuthenticationUserActionFilter());
    ...
 }

File AuthenticationUserActionFilter.cs:

public class AuthorizeUserActionFilter : System.Web.Mvc.Filters.IAuthenticationFilter
{
  public void OnAuthentication(AuthenticationContext filterContext)
  {
    bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true) || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousActionFilter), inherit: true);

    if (skipAuthorization) // anonymous filter attribute in front of controller or controller method
      return;

    // does this always read value from ASPXAUTH cookie ?
    bool userAuthenticated = filterContext.HttpContext.User.Identity.IsAuthenticated;

  if (!userAuthenticated)
  {
    filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
    return;
  }

  if( HttpContext.Current.User as Contracts.IUser == null )
  {
    // check if IUser is stored in session otherwise retrieve from db
    // System.Web.HttpContext.Current.User is reseted on every request.
    // Is it ok to set it from Session on every request? Is there any other better approach?
    if (HttpContext.Current.Session["User"] != null && HttpContext.Current.Session["User"] as Contracts.IUser != null)
    {
      HttpContext.Current.User = HttpContext.Current.Session["User"] as Contracts.IUser;
    }
    else
    {
      var service = new LoginService();
      Contracts.ISer user = service.GetUser(filterContext.HttpContext.User.Identity.Name);

      HttpContext.Current.Session["User"] = user;
      HttpContext.Current.User = user;
    }
  }
}

public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) {}

}

My login code is like this (in AccountController.cs):

[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
    LoginService service = new LoginService();
    Contracts.IUser user = service .Login(username, password);

    System.Web.HttpContext.Current.Session["User"] = value;
    System.Web.HttpContext.Current.User = value;

    // set cookie i.e. ASPX_AUTH, if remember me, make cookie persistent, even if user closed browser
    if (System.Web.Security.FormsAuthentication.IsEnabled)
      System.Web.Security.FormsAuthentication.SetAuthCookie(username, rememberMe);

    return new SuccessResponseMessage().AsJsonNetResult();
}

Contracts.IUser interface:

  public interface IUser : IPrincipal
  {
    Contracts.IUserInfo UserInfo { get; }
    Contracts.ICultureInfo UserCulture { get; }
  }

My question is this:

System.Web.HttpContext.Current.User is reseted on every request. Is it ok to set HttpContext.Current.User with Session value on every request? Is there any other better approach? What is best practise? Also Microsoft seems to have multiple ways of dealing with this problem (googled a lot of articles on this, also on stackoverflow Custom Authorization in Asp.net WebApi - what a mess?). There is a lot of confusion about this, although they developed a new Authorization in asp.net core.

Upvotes: 1

Views: 9671

Answers (1)

Darin Dimitrov
Darin Dimitrov

Reputation: 1039498

One possible approach is to serialize the user as part of the UserData portion of the ASPXAUTH cookie. This way you don't need to fetch it from the database on each request and you don't need to use Sessions (because if you use sessions in a web-farm you will have to persist this session somewhere like in a database, so you will be round-tripping to the db anyway):

[Filters.AllowAnonymousActionFilter]
[HttpPost]
public JsonResult Login(string username, string password, bool rememberMe = false)
{
    LoginService service = new LoginService();
    Contracts.IUser user = service.Login(username, password);

    string userData = Serialize(user); // Up to you to write this Serialize method
    var ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddHours(24), rememberMe, userData);
    string encryptedTicket = FormsAuthentication.Encrypt(ticket);
    Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket));

    return new SuccessResponseMessage().AsJsonNetResult();
}

And then in your custom authorization filter you could decrypt the ticket and authenticate the user:

public void OnAuthentication(AuthenticationContext filterContext)
{
    ... your stuff about the AllowAnonymousActionFilter comes here

    var authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie == null)
    {
        // Unauthorized
        filterContext.Result = new RedirectToRouteResult(new System.Web.Routing.RouteValueDictionary() { { "controller", "Account" }, { "action", "Login" } });
        return;
    }

    // Get the forms authentication ticket.
    var authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    Contracts.ISer user = Deserialize(authTicket.UserData); // Up to you to write this Deserialize method -> it should be the reverse of what you did in your Login action

    filterContext.HttpContext.User = user;
}

Upvotes: 3

Related Questions