Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ASP.NET Membership Login redirecting to unauthorized when user is logged in

We have an application that uses ASP.net Membership to provide the basic login mechanisms. It all works fine, but recently we discovered that if you try to go to the login page whilst logged in you are redirected to the 'Unauthorized' page.

Example user flow.

User goes to secured page (whole application requires login, there's not even a home page you can visit, just redirects straight to login). This redirects them to https://www.example.com/Account/Login.

User logs in and is redirected to home page https://www.example.com/. They are logged in and everything works fine.

User clicks a bookmark that happens to be set to https://www.example.com/Account/Login

User is redirected to generic Unauthorized page.

I have the <Authorize()> attribute on my AccountController but the <AllowAnonymous()> attribute on the 'Login' action, which as we saw earlier, works fine when you are not logged in, but when you are it seems to get in a bit of a muddle.

AccountController

<Authorize()> _
Public Class AccountController
'''other functions go here'''

<AllowAnonymous()> _
Public Function Login(ByVal returnUrl As String) As ActionResult
    ViewData("ReturnUrl") = returnUrl
    Return View()
End Function

AuthorizeRedirect filter

<AttributeUsage(AttributeTargets.[Class] Or AttributeTargets.Method)> _
Public Class AuthorizeRedirect
    Inherits AuthorizeAttribute
    Private Const IS_AUTHORIZED As String = "isAuthorized"

    Public RedirectUrl As String = "~/Home/Unauthorized"

    Protected Overrides Function AuthorizeCore(httpContext As System.Web.HttpContextBase) As Boolean
        Dim isAuthorized As Boolean = MyBase.AuthorizeCore(httpContext)

        httpContext.Items.Add(IS_AUTHORIZED, isAuthorized)

        Return isAuthorized
    End Function

    Public Overrides Sub OnAuthorization(filterContext As AuthorizationContext)
        MyBase.OnAuthorization(filterContext)

        Dim isAuthorized = If(filterContext.HttpContext.Items(IS_AUTHORIZED) IsNot Nothing, Convert.ToBoolean(filterContext.HttpContext.Items(IS_AUTHORIZED)), False)

        If Not isAuthorized AndAlso filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated Then
            filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl)
        End If
    End Sub
End Class

Seeing all this I thought the simplest solution would be to check if the user is already logged in on my Login action and redirect them away myself, something like this.

<AllowAnonymous()> _
Public Function Login(ByVal returnUrl As String) As ActionResult
    If User.Identity.IsAuthenticated() Then
        Return RedirectToAction("Index", "Home")
    End If
    ViewData("ReturnUrl") = returnUrl
    Return View()
End Function

But the AuthorizeFilter always jumps in the way first, which is understandable, but I can't quite figure out the last missing piece. All I want is it to not show the 'You don't have permission to view this page' if the user goes to the login screen when logged in, and rather redirect them to home page. What am I missing?


Edit to make things a bit clearer

When already logged in, I go to /Account/Login. This 302 redirects me to /Home/Unauthorized (my custom page). However, I am still logged in.

Network requests

Network request to Login page, which 302 redirects to Unauthorized

Unauthorized page. Notice the highlighted yellow sections show I am still logged in. This only appears if you are logged in. When not logged in you get none of that.

Unauthorized page

The problem seems to be that the application does not know what to do when I am already logged in and trying to go to a page that has the [AllowAnonymous] attribute on it. If anything, the behaviour I am seeing here is preferable to it actually giving me a login page again, because that would be confusing, but still, its not ideal.


Edit 2 - Stepping through the code line by line

Here are the results of stepping through the code line by line.

Page /Account/Login whilst logged in.

First breakpoint in OnAuthorization sub in AuthorizeRedirect filter.

Public Overrides Sub OnAuthorization(filterContext As AuthorizationContext)
    MyBase.OnAuthorization(filterContext)

    Dim isAuthorized = If(filterContext.HttpContext.Items(IS_AUTHORIZED) IsNot Nothing, Convert.ToBoolean(filterContext.HttpContext.Items(IS_AUTHORIZED)), False)

    If Not isAuthorized AndAlso  filterContext.RequestContext.HttpContext.User.Identity.IsAuthenticated Then
        filterContext.RequestContext.HttpContext.Response.Redirect(RedirectUrl)
    End If
End Sub

Line starting with Dim isAuthorized returns False. filterContext.HttpContext.Items(IS_AUTHORIZED) is nothing (does not exist in list of items).

This then means the next If statement evaluates to True (Not isAuthorized AndAlso ...IsAuthenticated), resulting in the redirect to RedirectUrl.

After this happens it appears to go back over the same steps, except this time it evaluates to false, meaning the redirect doesn't occur, although I'm guessing this is just the 'Unauthorized' page loading and running through the same code again.

I attempted to add the following block to the top of the Login function of the AccountController.

    If User.Identity.IsAuthenticated() Then
        Return RedirectToAction("Index", "Home")
    End If

But, of course, since the filter is run before the actions occur this code isn't hit until after it's already redirected me to Unauthorized(verified by stepping through).

like image 844
Rob Quincey Avatar asked Jul 28 '16 15:07

Rob Quincey


1 Answers

The base class for AuthorizationAttribute has this code in its OnAuthorization method:

bool skipAuthorization = filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
                         || filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);

if (skipAuthorization)
{
    return;
}

if (AuthorizeCore(filterContext.HttpContext))
// ...

As such if the controller action has an AllowAnonymousAttribute defined upon it, your AuthorizeCore method will not get called.

Because of that filterContext.HttpContext.Items(IS_AUTHORIZED) will never get set.

You could simply copy the code from here to implement OnAuthorization without calling the base class. This way you could deal with the caching in which ever way you want.

Incidentally, I was under the impression that if authorization failed that a later process in the request pipeline did a redirect to the login page anyway. This is why the base implementation of OnAuthorization sets the filterContext.Result to a new HttpUnauthorizedResult instance. So it is not entirely clear why you are overriding OnAuthorization and doing the redirect in the first place. If you want some kind of custom authorization code simply returning true or false from AuthorizeCore ought to be enough.

like image 98
Martin Brown Avatar answered Oct 06 '22 16:10

Martin Brown