views:

68

answers:

1

I have decorated my controller with an Authorize attribute, as so:

[Authorize(Roles="ExecAdmin")]

If I try to go to that controller after logging in as a user who is not ExecAdmin, it does appear to be attempting to redirect to a login page. BUT, the page it is attempting to redirect to is not my login page, it is a view called LogOnUserControl.ascx. This is a partial view that is not displayed by my login page.

I have no idea why it is doing this -- or maybe it is trying to redirect to some other page altogether, one which does display LogOnUserControl.ascx. Or maybe it is looking for anything with "LogOn" in the name? (Though the name of my login view is LogOn.aspx...)

How can I tell it what page to redirect to?

UPDATE: I do have this in the global.asax

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    HttpCookie authCookie = Context.Request.Cookies[FormsAuthentication.FormsCookieName];
    if (authCookie == null || authCookie.Value == "")
    {
        return;
    }
    FormsAuthenticationTicket authTicket = null;
    try
    {
        authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    }
    catch
    {
        return;
    }
    string[] roles = authTicket.UserData.Split(new char[] { ';' });
    //Context.ClearError(); 
    if (Context.User != null)
    {
        Context.User = new System.Security.Principal.GenericPrincipal(Context.User.Identity, roles);
    }
}

... since I am using a non-standard way of defining roles; i.e., I am not using ASP.NET membership scheme (with role providers defined in web.config, etc.). Instead I am setting roles this way:

// get user's role
string role = rc.rolesRepository.GetUserType(rc.loginRepository.GetUserID(userName)).ToString();

// create encryption cookie
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
        1,
        userName,
        DateTime.Now,
        DateTime.Now.AddMinutes(120),
        createPersistentCookie,
        role //user's role 
        );

// add cookie to response stream
string encryptedTicket = FormsAuthentication.Encrypt(authTicket);

System.Web.HttpCookie authCookie = new System.Web.HttpCookie(FormsAuthentication.FormsCookieName, encryptedTicket);
System.Web.HttpContext.Current.Response.Cookies.Add(authCookie);

(This is called after the user has been validated.)

Not sure how this could be impacting the whole thing, though ...

UPDATE: Thanks to Robert's solution, here's how I solved it -- extend AuthorizeAttribute class:

public class AuthorizeAttributeWithMessage : AuthorizeAttribute
{
    private string _message = "";
    public string Message
    {
        get { 
            return _message; 
        }
        set { 
            _message = value;
        }
    }

    protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            // user is logged in but wrong role or user:
            filterContext.Controller.TempData.Add("Message", Message);
        }
        base.HandleUnauthorizedRequest(filterContext);
    }
}

Then in the LogOn view:

<% 
    if (HttpContext.Current.Request.IsAuthenticated)
    {
        // authenticated users should not be here
        Response.Redirect("/Home/Index");
    }
%>

And in the home page view:

<% if (TempData != null && TempData.Count > 0 && TempData.ContainsKey("Message"))
   { %>
<div class="largewarningtext"><%= TempData["Message"]%></div>
<% } %>

And atop the affected controllers:

[AuthorizeAttributeWithMessage(Roles = "Consultant,ExecAdmin", Message = "You do not have access to the requested page")]

This has the advantage of ALWAYS redirecting any authenticated user who ends up on Logon.aspx -- authenticated users should not be there. If there is a message in the TempData, it will print it out on the home page; if not, it will at least have done the redirect.

+3  A: 

Login page is configured within web.config file.

But you probably already know that. The real problem here is a bit more complicated. I guess you're onto something very interesting here, since Login page barely authenticates a user. It doesn't check its authorization for a particular resource (which is your case here where authorization fails) so this shouldn't redirect to login page in the first place.

Checking AuthorizeAttribute source code, you should get a 401: Unauthorize Request response from the server. It doesn't redirect you to the login page (as I anticipated in the previous paragraph, since login is too stupid for that. So there most be something else in your code that doesn't work as it should.

Edit

As this page states:

If the site is configured to use ASP.NET forms authentication, the 401 status code causes the browser to redirect the user to the login page.

Based on this information it's actually forms authentication that sees this 401 and redirects to login (configured as you described in the comment).

But. It would be nice to present some message to the user why they were redirected to login page in the first place. No built-in functionality for that... Still this knowledge doesn't solve your problem, does it...

Edit 2

There are two patterns you can take that actually look very similar to the user, but work diferently on the server.

Simpler one

  1. Write your own authorization attribute (simply inherit from the existing one and add an additional public property Message to it), where you can also provide some sort of a message with attribute declaration like ie.

    [AuthorizeWithMessage(Role = "ExecAdmin", Message = "You need at least ExecAdmin permissions to access requested resource."]
    
  2. Your authorization attribute should populate TempData dictionary with the provided message (check documentation about TempData that I would use in this case) and then call into base class functionality.

  3. change your login view to check for the message in the TempData dictionary. If there is one, you can easily present it to the already authenticated user (along with a link to some homepage that they can access), so they will know why they are presented with a login.

Complex one

  1. create your own authorization filter (not inheriting from the original) and provide your own redirection to some authorization login view that would serve the login in case user has insufficient rights.

  2. create your custom login view that can in this case be strong type. Your authorization filter could populate it with the correct model. This model will include the message string and it can also provide the route link to a page where a user can go.

  3. custom configuration classes that serve this configuration of custom login page.

You could as well configure various different route definitions based on user rights. So for some rights they'd be presented with some page, but if they have some other rights, their route would point to a different route.

Which one to choose?

Go with the simpler one if it satisfies your needs, but if you want more control of the whole process I'd rather go with the complex one. It's not so complicated and it would give you full control of the insufficient login process. You could make it a much better experience for the users.

Robert Koritnik
Like this?<authentication mode="Forms"> <forms loginUrl="~/Account/LogOn" defaultUrl="~/Home/Index" timeout="2880"/></authentication>I have this but it does not seem to be working.
Cynthia
@Cynthia: it obviously does, since you were able to login as a user with non ExecAdmin role.
Robert Koritnik
Well, yes, obviously it does work to redirect for logins. What I meant was, it does not seem to be working for redirect when the Authorize test fails.
Cynthia
OK, I just saw your added comment. Let me say that, when I comment out the offending code in LogOnUserControl.ascx (the code that was causing an null reference exception), it DOES go to the login page (though I still have no idea why it was going to LogOnUserControl.ascx, since it's not referenced in LogOn.aspx.) However, this is really not what I want it to do, since the "returnURL" parameter is the page from which I just got redirected, the one that I had no access to .. so logging on as the same user just sends me to the same page, which kicks me back to the login page ... etc.
Cynthia
How can I debug this, how can I find out what's going on??
Cynthia
Have you tried deleting that control, since you're not using it. Do you get the "view not found" exception as well?
Robert Koritnik
Do you maybe have some special action result filter that check unauthorised responses? or maybe an HttpModule?
Robert Koritnik
@Cynthia: How to debug or profile this? Start Fiddler and see what's going on... That's for a start. There should be one 302...
Robert Koritnik
Robert, please see the additional info I added to my question ... (though I'm not sure it helps).What is Fiddler?
Cynthia
@Cynthia: Fiddler is a great program (a proxy actually) that helps you analyse HTTP traffic and profile issues you may have. It's a hard-code developer's tool to see what's going on behind the scenes. http://www.fiddler2.com/fiddler2/
Robert Koritnik
@Cynthia: At first glance, your code doesn't interfere with the authorisation process. It's just a custom way of handling your authentication (as mentioned you're not using membership as many developers don't as well).
Robert Koritnik
@Cynthia: Is the `?ReturnUrl=` query string (in the URL) correctly set to your controller's action with AuthorizeAttribute?
Robert Koritnik
Well, originally it is ReturnUrl=%2fdefault.aspx%3fthen after the redirect, it's ReturnUrl=%2fManage, which resolves to Manage/Index, which is the correct page (the page my user illegally accessed). The Authorize attribute is actually set for the whole controller, not a specific action.
Cynthia
P.S. I have downloaded Fiddler and will give it a try tomorrow.
Cynthia
Fiddler may not help you with this problem (it seems fine from the HTTP side by the info you've provided), but it's probably still a good idea to get acquainted with Fiddler. It will save you some other time as well. (BTW: make sure you don't access you app via *localhost* address but rather via machine name)
Robert Koritnik
OK, I discovered why the partial view is being accessed -- it's in the master page that the login page uses (I had forgotten). The part that was giving an error is not supposed to be executed unless the request is authenticated. The error is a null object reference when I try to access a ViewData field that does not exist because I never gave it a value on the login page, never thinking that an authenticated user could get to the login page. So -- something seems screwy here. Redirecting to the login page, when the user is still logged in and still authenticated, does not seem desirable.
Cynthia
@Robert -- Is there any way I can handle this exception and send it where I want it to go? You quoted "the 401 status code causes the browser to redirect the user to the login page". The browser does this? Is there some way I can tell it to go somewhere else on a 401 error? (Not too familiar with this browser-level error handling.)
Cynthia
@Cynthia: No the browser barely does the 302 redirection that gets from the server. Forms authentication does it. The way I suggest you do it is still display the login page, but provide some information to the user, they don't have sufficient permissions to access the page they wanted. And that they can login with a different username. That would be the best way of handling this situation.
Robert Koritnik
That would imply that I can at least test for a 302 redirect within the Login view so I can display a proper message. But what I'd like to do is "interrupt" the process in some way to log the user out before displaying the Login page. And to change the "ReturnURL" parameter so it does not simply return to the unauthorized page if the person logs in under the same name. But I guess that's not possible using the [Authorize] attribute.
Cynthia
I'm beginning to think the Authorize attribute is less than useful if you're just testing for an unauthorized role, not a totally unauthenticated user. Maybe I should go back to doing what I was doing before -- at the beginning of each action I called a static method with the user's role and the name of the action, and the method returned yea or nay for access. I should mention that there is no way a user can get to an unauthorized page by clicking on a link -- only by typing the route, and most of them would have no idea to do that. So this is functionality is perhaps not critical.
Cynthia
@Cynthia: Check my last edit. This should help you solve it the best possible way. (BTW: If my answer does solve your problem, you can always mark it as accepted) ;)
Robert Koritnik
@Robert -- Sounds very interesting! The problem I'm having with solution (1) is, it doesn't seem that easy to get things into the TempData dictionary, which seems to be connected in some way with a controller. That is, a controller knows about TempData, but the AuthorizeAttribute class hasn't a clue -- or at least I can't find the handle into it.
Cynthia
OK, I got it! I had to override the HandleUnauthorizedRequest method in order to access the filterContext.Controller (ControllerBase) which has the TempData property.I did try solution (2), but this part: "provide your own redirection to some authorization login view" proved too daunting, though I'm sure it could be done, but I'm running out of time!Thank you very much Robert!
Cynthia
P.S. My solution is added as an edit to my question.
Cynthia
@Cynthia: I'm glad you've solved it. One more thing: attribute classes should always have the word "Attribute" at the end. And when you put them on your controller/action, you can easily omit the word attribute. In your case it should be: `AuthorizeWithMessageAttribute`. And FYI: just as much Fiddler can help you, there's another tool that's even **more** helpful. .Net Rreflector http://www.red-gate.com/products/reflector/ It helps you understand how things work and if you've not used it before, I suggest you look into it as well.
Robert Koritnik
Thanks Robert, I do have Reflector -- in fact I used it to great advantage in figuring out how Microsoft did this attribute thing. Also, I have changed the name to put Attribute at the end. Again, thanks for your help!
Cynthia