Handle Ajax request with expired forms authentication, in MVC ajax.form

There is a situation where I display a screen dynamically load a partial view using ajax.

And when the authentication expires, user initiates an ajax call, then the action is being called without authorization.

It actually returns a login view inside the DOM object on the page rather than returns a expected content with partial view.

The users will have bad UI experience and get confused.

Here I demonstrate the scenario to show you what exactly happens and what we can do about it.

1. Create an ajax.form which simply returns a partial view.


@using (Ajax.BeginForm("AjaxTestNoHandle", "Home", new AjaxOptions { UpdateTargetId = "testcontainer2"}, new { id="test-form"}))
{ 
    
}

<div id="testcontainer2">
</div>

2. Create the action method with “Authorize” attribute and return a partial view “TestPartial”

  [Authorize]
        public ActionResult AjaxTestNoHandle()
        {
            return PartialView("TestPartial");
        }

3. Run the application and click button “No Redirection Handling”. Because the user is not authenticated and it returns a partial view with the whole “Login” page. (I made login page with black background for displaying purpose)

image

Now let’s create  a customized authorize attribute to handle this situation.

1.  Create a CustomizedAuthorizeAttribute to handle login redirection if authentication is expired.  I only override the action HandleUnauthorizedRequest to detect if request is ajax, return a json result with a redirect url.

    public class CustomizedAuthorizeAttribute : AuthorizeAttribute
    {
        public override void OnAuthorization(AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
        }

        protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
        {
            string url = string.Format("{0}?returnUrl={1}",System.Web.Security.FormsAuthentication.LoginUrl,
                filterContext.HttpContext.Server.UrlEncode(filterContext.HttpContext.Request.RawUrl));
            if (filterContext.HttpContext.Request.IsAjaxRequest())
            {
                var redirectResult = filterContext.Result as RedirectResult;
                if (filterContext.Result is RedirectResult)
                {
                    // It was a RedirectResult => we need to calculate the url
                    var result = filterContext.Result as RedirectResult;
                    url = UrlHelper.GenerateContentUrl(result.Url, filterContext.HttpContext);
                }
                else if (filterContext.Result is RedirectToRouteResult)
                {
                    // It was a RedirectToRouteResult => we need to calculate
                    // the target url
                    var result = filterContext.Result as RedirectToRouteResult;
                    url = UrlHelper.GenerateUrl(result.RouteName, null, null, result.RouteValues, RouteTable.Routes, filterContext.RequestContext, false);
                }
                filterContext.Result = new JsonResult
                {
                    Data = new { Redirect = url },
                    JsonRequestBehavior = JsonRequestBehavior.AllowGet
                };
            }
            else
            {
                //non-ajax request
                base.HandleUnauthorizedRequest(filterContext);
            }
            
        }

2. Create controller action method with CustomizedAuthorize

        [CustomizedAuthorize]
        public ActionResult AjaxTest()
        {
            return PartialView("TestPartial");
        }

3. Create ajax.form post to “AjaxTest” method.


@using (Ajax.BeginForm("AjaxTest", "Home", new AjaxOptions { UpdateTargetId = "testcontainer" }, new { id="test-form"}))
{ 
    
}

<div id="testcontainer">
</div>

4. Click “Redirection Handling” button on the page without login. In FireBug – Console, you can see a json is returned

{"Redirect":"/Account/Login?returnUrl=%2fHome%2fAjaxTest%3fLength%3d4"}

5. Now let’s add a javascript function to handle the json and redirect to the returned url. Add a OnSuccess event in “AjaxOptions” call “validate”


@using (Ajax.BeginForm("AjaxTest", "Home", new AjaxOptions { UpdateTargetId = "testcontainer", OnSuccess="validate" }, new { id="test-form"}))
{ 
    
}

<div id="testcontainer">
</div>

6. Create javascript function validate.


    function validate(data) {
        if (data.Redirect)
            window.location = data.Redirect;
    }

7. Here you go, all done! When you click the “Redirection Handling” button, it will detect the forms authentication login url and return to the login page inside the ajax request!

Cools!

To download the demo project, go to codeplex https://ajaxrequestauthentic.codeplex.com/

Advertisements

Implement FormsAuthentication with custom identity in asp.net/ MVC

Sometimes you want to save more data into the authentication cookie rather than retrieve them from database all the time.

So I come up this way to create a custom Identity to allow you retrieve extra data from authentication cookies.

1. To create an identity, you need to implement the interface IIdentity.

I added Email & Roles as properties of the MyIdentity. And retrieve the data from FormsAuthenticationTicket.UserData (which is passed into MyIdentity’s constructor)


public class MyIdentity : IIdentity
 {
private System.Web.Security.FormsAuthenticationTicket ticket;

public MyIdentity(System.Web.Security.FormsAuthenticationTicket ticket)
{
this.ticket = ticket;
}

public string AuthenticationType
{
get { return "MobileOrder"; }
}

public bool IsAuthenticated
{
get { return true; }
}

public string Name
{
get { return ticket.Name; }
}

public string Email
{
get
{
string[] data = ticket.UserData.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (data.Length > 0)
return data[0];
else
return "";
}
}

public string[] Roles
{
get
{
string[] data = ticket.UserData.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);
if (data.Length > 1)
{
string[] roles = data[1].Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
return roles;
}
else
return new string[] { "User" };
}
}
 }

2. I created an AuthenticationHelper to create the cookie and assign the extra data to the Ticket’s UserData.


public class AuthenticationHelper
{
public static void CreateAuthCookie(int id, string email, DateTime expiration, string[] roles, bool rememberMe)
{
FormsAuthenticationTicket authTicket = new
FormsAuthenticationTicket(1, //version
id.ToString(), // user name
DateTime.Now,
expiration, //Expiration
rememberMe, //Persistent
CreateAuthTags(email, roles));

string encTicket = FormsAuthentication.Encrypt(authTicket);

HttpContext.Current.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
}

public static string CreateAuthTags(string email, string[] roles)
{
StringBuilder sb = new StringBuilder();
sb.Append(email);
sb.Append("|");
for (int i = 0; i < roles.Length; i++)
{
sb.Append(roles[i]);
if (i < roles.Length - 1)
sb.Append(",");
}
return sb.ToString();
}
}

3. In Global.asax, register MvcApplication_PostAuthenticateRequest event to cast generic Identity to MyIdentity and assign back to the Principal in HttpContext.Current.User


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);
GenericPrincipal prin = new GenericPrincipal(id, id.Roles);
HttpContext.Current.User = prin;
}
}
}

4.  To read the MyIdentity from your pages or views

((MyIdentity)HttpContext.Current.User.Identity).Email;

Alternatively you can create PartialView for the login status and put it in _Layout.cshtml. eg.


@if(Request.IsAuthenticated) {

<ul>
<li class="small">@(((Mobile.Order.Web.Common.Identity.MyIdentity)HttpContext.Current.User.Identity).Email)</li>

|

<li>@Html.ActionLink("Account", "Index", "Account")</li>

|

<li>@Html.ActionLink("Log Off", "LogOff", new { controller = "Account", returnUrl = HttpContext.Current.Request.RawUrl })</li>

</ul>

}

else

{

<ul>

<li>

@*@Html.ActionLink("Login", "Login", "Account")*@

<a href="/Account/AjaxLogin" id="loginButton">Login</a>

</li>

|

<li>@Html.ActionLink("Register", "Index", "Register")</li>

</ul>

}