Using Roles authentication in ASP

Using Roles authentication in ASP

Old forum URL: forums.lhotka.net/forums/t/981.aspx


jwooley posted on Tuesday, August 22, 2006

I'm having difficulty using roles in my forms authentication in an ASP 2.0 application. I have set up my custom Principal and Identity objects and am persisting them into the CSLA.ApplicationContextUser in the global.asax. I have set the security on the main site with the following in Web.config:

<authentication mode="Forms">
  <
forms loginUrl="Login.aspx" name="CorrespondentWeb"/>
</
authentication>
<
authorization>
  <
deny users="?" />
  <
allow users="*" />
</
authorization>

In the child path, I have tightened the authentication to only allow "Admin" users to access that portion as follows:

<authorization>
 
<allow roles="Admin"/>
  <
deny users="*"/>
</
authorization>

I set the SiteMap to use security trimming and that part appears to be working correctly. However, when a user who is in the "Admin" role tries to access one of the files in that directory, they are bumped back to the login page. I have checked the security and the .IsInRole is not being called on page access as I would expect. It is being called by the Menu to check the security trimming properly.

In reading more about role management, I have tried to add a custom RoleProvider. The GetRolesForUser method is called (apparently by the forms authentication). Here is where the fun exists. Instead of just checking the .IsInRole or IsUserInRole method, it requires access to the entire string array of the roles which is not exposed by the Principal or Identity. Thus, I extended Principal and Identity to expose the mRoles as a string array publicly from the Principal and Friend from Identity (so that Principal could pass it on through). Here is what I came up with then for GetRolesForUser:

Public Overrides Function GetRolesForUser(ByVal username As String) As String()
 
If CSLA.ApplicationContext.User.Identity.Name <> username Then
   
Dim blank(0) As String
   
Return blank
 
Else
   
Return CType(CSLA.ApplicationContext.User, LarsBo.Security.BusinessPrincipal).Roles
 
End If
End Function

The catch here, is the CSLA.ApplicationCondext.User which just exposes HttpContext.Current.User has a different underlying datatype than other references to the object. In this case it is holding a System.Web.Security.RolePrincipal containing a System.Web.Security.FormsIdentity object. Apparently, these are populated from a clone of the original Principal/Identity and replaced them at some point. Since they don't have a reference to the original Principal, I can't cast it to my custom BusinessPrincipal which exposed the Roles string array.

I really don't want to have to run back to the database to pull information which is already in memory in order to get this functionality. I have spent the last couple days hitting my head into the wall and could use a little assistance. Please help me get rid of this headache.

Jim Wooley
http://devauthority.com/blogs/jwooley

JoeFallon1 replied on Tuesday, August 22, 2006

Jim,

I have not gone down that road but here are some comments.

1. Has the Admin already logged in once and *then* gets kicked back to the login page when trying to access forms in the child path? Or is the Admin expecting to access those forms without logging in? (I think the first behavior is expected, not the 2nd one.)

2. I have not built a custom Role Provider. But from what I have read, I do not recall ever hearing about the need for a string array of Roles. I would do more research and attempt to eliminate that requirement completely.

3. I am concerned about CSLA.ApplicationCondext.User containing a clone of the original Principal/Identity objects. If I can't cast to my custom Principal in CSLA 2.0 then I am going to have some serious issues. (I am still in the planning stages for moving to 2.0.) I tried it out on a local data portal and it all worked as expected. No cloning occurred. Are you remoting? Is that when the cloning happened?

Good luck.

Joe

 

 

 

jwooley replied on Tuesday, August 22, 2006

Joe,

1: Yes. The admin has already logged in prior to trying to access the restricted page.

2: I'm with you and hope someone comes up with an alternative that I am overlooking.

3: Typically, I have no problems casting the CSLA.ApplicationContext.User to my custom principal. The problem only appears during the RoleProvider method.

FYI, I did check ProjectTracker, and it does not include an implementation of declarative Forms Authentication with role based permissions in the config file. There must be a way.

Jim

Diz replied on Thursday, December 28, 2006

Hi Jim,

Did you ever figure this out?  I'm in the same boat. 

Thanks!

jwooley replied on Thursday, December 28, 2006

No, I didn't come up with a good solution. I did blog the issue but didn't get much in terms of response there either. I ended up checking it on page load of the invalid page and doing a redirect. It would be nice if the security trimming would play nice with the custom principal, but it appears this is not the case.

Jim

Diz replied on Thursday, December 28, 2006

The strange thing to me is this works within a page:

            if (this.Context.User.IsInRole("Admin"))

but this in the config doesn't work:

        <allow roles="Admin"/>

I would have thought the two would result in the same call. 

This also works:

        <allow users="tdaurizi"/>

Which to me proves that the config's auth tag can get to my Csla provided identity.  So there must be a different call that asp.net is making when it encounters that roles authorization tag...

jwooley replied on Thursday, December 28, 2006

Indeed, the menu's security trimming uses the RoleProvider rather than the custom principal's MembershipProvider. I presume this is done to enable caching the roles as a delimited list on the client's cookie for faster evaluation rather than needing to access the MembershipProvider cached on the server in a session (in case session is disabled). I was able to trace this via Reflector to confirm the behavior. I wrote it up a while back at http://devauthority.com/blogs/jwooley/archive/2006/08/25/2207.aspx. Check the comments on that post for more info as well.

Jim Wooley

JCannelos replied on Thursday, February 22, 2007

Guys,

I guess I'll toss my two cents in here because I have been wrestling with this all morning. :( I have noticed in general that when using Forms Authentication - the AquireRequestState event is fired several times. Only the first time is Session State available (throws error otherwise).

On the first call, I test for valid Session State and test for my Csla principal object in Session. If found, I restore it to Csla.ApplicationContext.User and I'm done. If my Csla principal object is NOT in Session, I then test for the FormsAuthenticationTicket in cookie and can log the user in that way (userName only log in). I discovered this neat trick from other posts in this forum.

Once I've logged in, AquireRequestState starts firing more often on each page request. On these calls (other than the first), any call to Session returns an error, which I trap, and leave the event.

The kicker here is that if you look at the HttpContext.Current.User object at this time, the object itself is a GenericPrincipal --> FormsIdentity. It shows that I am Authenticated (which is good), but of course there are no roles - which causes me to return to the Login page if I access a page that has <Location><allow roles="Admin" /> (ignore bad schema here) on it in web.config.

So its apparent that sometime during the life cycle, FormsIdentity is in charge, and it does not have any idea of my user roles. In reading some more posts, I've found that I can stash a string[] of Roles in the UserData section of the FormsAuthenticationTicket, which is in turn placed in a persistent or not persistent cookie. This means that like the original poster, I'd have to expose a Roles property from my Csla Principal object. With this in hand, I could recreate the Principal object like so:

if (HttpContext.Current.Request.IsAuthenticated && HttpContext.Current.User.Identity is FormsIdentity)

string[] roles = new string[] { "Admin" };            //would normally come from FormsAuthenticationTicket.UserData.Split("|") --> decrytped cookie. testing only.

HttpContext.Current.User = new GenericPrincipal(HttpContext.Current.User.Identity, roles);

I would have NO problem doing this if it actually worked. It would come to the rescue when Session state was not available in AquireRequestState and make sure that the roles were being applied during the page (web.config roles) authorization process.

However this seems to have no effect whatsoever (sigh!). I've even placed the code in the AuthorizeRequest event because I've heard that AquireRequestState can occur too late for role based authorization.

Anyways, sorry for the long post... Rocky (or someone) to the rescue? :P

J'son

JCannelos replied on Monday, February 26, 2007

Quick update... it turns out the multiple calls to AquireRequestState was for my AJAX scripts (WebResource.axd, et) - this is when Session State is unavailble. So now I'm not sure if AJAX + web.sitemap with securityTrimming + roles in web.config + CSLA security objects will all work together.

Still back to square one... :(

J'son

rrahlf replied on Wednesday, February 28, 2007

I'm really glad to see this post.  I'm just about to go through the same thing.  This is my first app with CSLA.  I'm going to be doing a simple sign on, just register and be signed on forever - no password, using forms authentication, custom CSLA security objects, and the site will also use AJAX .NET.

Please post any results any of you find, and I'll do the same.  I'm looking forward to finding a solution to our problem.

pblaylock replied on Thursday, March 01, 2007

I spent a frustrating period at the beginning of my current project tackling what sounds like a similar problem. It was a while ago I worked on this, so some of the details may not be exactly correct. I remember that I came to the conclusion that the roles check, as specified in the web.config file, happens before the Session State is restored.

The web.config role checks seem to happen on or after AuthenticateRequest, so you need the CSLA principle available at this point. If you are restoring the Principle from Session then it will not be available until AquireSessionState, which occurs after Authenticate Request. Bit of a catch 22.

The work around I came up with looks like this (I did this in a HttpHandler, but the same logic should work in Global.asax, also this is based on CSLA 1.0) ....

Store the roles in the cookie. In AuthenticateRequest load the roles from the cookie and put this into a Generic prinicple, this should allow the web.config role checks to work.

    private void app_AuthenticateRequest(object sender, EventArgs e)
        {
HttpCookie cookie = HttpContext.Current.Request.Cookies[FormsAuthentication.FormsCookieName];

            if(cookie == null)
                return;
           
            FormsAuthenticationTicket ticket = null;

            try
            {
                ticket = FormsAuthentication.Decrypt(cookie.Value);
            }
            catch(Exception ex)
            {
                HttpContext.Current.Server.Transfer("~/login.aspx");
            }

            string[] roles = ticket.UserData.Split('|');
           
            FormsIdentity identity = new FormsIdentity(ticket);
            GenericPrincipal principal = new GenericPrincipal(identity, roles);
            HttpContext.Current.User = principal;
            Thread.CurrentPrincipal = HttpContext.Current.User;
}

In AcquireRequestState you can now get hold of the CSLA principal

        private void app_AcquireRequestState(object sender, EventArgs e)
        {
            if(HttpContext.Current.Session["CSLA-Principal"] != null)
            {
                Thread.CurrentPrincipal = (IPrincipal)HttpContext.Current.Session["CSLA-Principal"];
                HttpContext.Current.User = Thread.CurrentPrincipal;
            }
            else
            {
                // if the app domain resets on the server then the session objects will be lost,
                // but the user will still appear to be logged in according to the cookie, so
                // force the user to re-login.
                if(Thread.CurrentPrincipal.Identity.IsAuthenticated)
                {
                    System.Web.Security.FormsAuthentication.SignOut();
                    HttpContext.Current.Server.Transfer("~/login.aspx");
                }
            }
        }

This seems to work ok for me.




Copyright (c) Marimer LLC