3

I am building an ASP.NET UI on an existing system, which consists of separate SQL server databases for each project. An "enterprise" database lists all current projects which allows anonymous users to select the project to work in. The project name is stored in a session variable. When log in is required the username/password/roles etc are obtained from the database indicated by the project name. I have implemented my own basic membership and role providers to do this, with changes in web.config to specify the roles required for specific pages. (I do not use the standard ASP.NET Configuration tool to manage users, I have existing apps that work with my user tables).

This all seemed to work initially but I discovered that the session variables are not yet loaded at the time when the authorization system checks the roles the current user belongs to in order to determine if the page is accessible. So if we have a < allow roles="xxx" > in web.config then the authorization system fires before session data is loaded and thus before I know which project database should be used.

[Specifically: HttpContext.Current.Session is null when the call to RoleProvider.GetRolesForUser is made]

Anybody who has tackled this problem should know exactly what I'm talking about. My questions therefore are:

A) What is the "Best Practise" solution to this scenario?

B) Could I be storing the project name somewhere else (not in session variable) that is available during the authorization phase?

[Update: Yes - we can use cookies, assuming we do not require cookieless operation]

C) Is there a way to manually get the session variable at this earlier time?

I tried an option to cache roles in cookies, but after a few minutes of testing with that option on I found GetRolesForUsers was still being called.

Thanks

Update:
Here is another description of the root problem which suggests "The application could cache this information in the Cache or Application objects.":
http://connect.microsoft.com/VisualStudio/feedback/details/104452/session-is-null-in-call-to-getrolesforuser

Update:
This looks like the same problem found here:
Extending the RoleProvider GetRolesForUser()

Update:
There was a suggestion about using UserData in FormsAuthenticationTicket, but I require this data even when not logged on.

Community
  • 1
  • 1
Etherman
  • 1,777
  • 1
  • 21
  • 34
  • Where do you set the Auth? I tend to set the sessions in the MemberShip Provider if necessary and it usually works. – Eric Herlitz Jan 23 '12 at 09:37
  • When RoleProvider.GetRolesForUser() is called I find that HttpContext.Current.Sessions is null, meaning I do not know which database to look in for user details. – Etherman Jan 23 '12 at 09:54
  • I have solved the main problem using cookies. Instead of saving the project name to Session[] I save it to Response.Cookies[], and then use HttpContext.Current.Request.Cookies["ProjectName"].value inside GetRolesForUser(). For my current purposes this seems to work. However, I see this breaking cookieless functionality. – Etherman Jan 23 '12 at 12:45

2 Answers2

2

UPDATE: I solve this these days in a much simpler way by using a wildcard SSL certificate that allows me to configure subdomains for each project, thus the project selection is specified directly in the URL (and each project gets its own subdomain). I still use a cookie hack purely for testing purposes when running on localhost where we have no subdomains.

Original solution:

I have not found any "best practise" write up on this scenario, but here is what I have settled on:

1) In order to support anonymous users switching between projects (i.e. SQL databases) I simply use a session variable to track the project selection. I have a global property that uses this project selection to serve the corresponding SQL connection string as and when it is required.

2) In order to support the call to GetRolesForUser() on pages that have role restrictions applied to them we cannot use the session variable, because as stated the session variable has not been initialized yet when GetRolesForUser() is actually called (and I have found no way to force it into being at this early point in the request cycle).

3) The only option is to use a cookie, or use the Forms Authentication ticket's UserData field. I trawled through many theories about using session/cookie/IDs linked to an object stored in the application cache (which is available when the session is not) but ultimately the correct choice is to place this data in the authentication ticket.

4) If a user is logged on to a project it is via a ProjectName/UserName pair, hence anywhere we are tracking the user's authentication we require both these data. In trivial testing we can get away with the username in the ticket and the projectname in a separate cookie, however it is possible for these to get out of synch. For example if we use a session cookie for the projectname and tick "remember me" when we logon (creating a permanent cookie for the authentication ticket) then we can end up with a username but no projectname when the session cookie expires (browser is closed). Hence I manually add the project name to the UserData field of the authentication ticket.

5) I have not figured out how to manipulate the UserData field without explicitly setting a cookie, which means that my solution cannot work in "cookieless" session mode.

The final code turned out to be relatively simple.

I override the Authenticate event of the LoginView in the login page:

    //
    // Add project name as UserData to the authentication ticket.
    // This is especially important regarding the "Remembe Me" cookie - when the authentication
    // is remembered we need to know the project and user name, otherwise we end up trying to 
    // use the default project instead of the one the user actually logged on to.
    //
    // http://msdn.microsoft.com/en-us/library/kybcs83h.aspx
    // http://msdn.microsoft.com/en-us/library/system.web.ui.webcontrols.login.remembermeset(v=vs.100).aspx
    // http://www.hanselman.com/blog/AccessingTheASPNETFormsAuthenticationTimeoutValue.aspx
    // http://www.csharpaspnetarticles.com/2009/02/formsauthentication-ticket-roles-aspnet.html
    // http://www.hanselman.com/blog/HowToGetCookielessFormsAuthenticationToWorkWithSelfissuedFormsAuthenticationTicketsAndCustomUserData.aspx
    // http://stackoverflow.com/questions/262636/cant-set-formsauthenicationticket-userdata-in-cookieless-mode
    //
    protected void LoginUser_Authenticate(object sender, AuthenticateEventArgs e)
    {
        string userName = LoginUser.UserName;
        string password = LoginUser.Password;
        bool rememberMe = LoginUser.RememberMeSet;
        if ( [ValidateUser(userName, password)] )
        {
            // Create the Forms Authentication Ticket
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                1,
                userName,
                DateTime.Now,
                DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes),
                rememberMe,
                [ ProjectName ],
                FormsAuthentication.FormsCookiePath);

            // Create the encrypted cookie
            HttpCookie cookie = new HttpCookie(FormsAuthentication.FormsCookieName, FormsAuthentication.Encrypt(ticket));
            if (rememberMe)
                cookie.Expires = DateTime.Now.AddMinutes(FormsAuthentication.Timeout.TotalMinutes);

            // Add the cookie to user browser
            Response.Cookies.Set(cookie);

            // Redirect back to original URL 
            // Note: the parameters to GetRedirectUrl are ignored/irrelevant
            Response.Redirect(FormsAuthentication.GetRedirectUrl(userName, rememberMe));
        }
    }

I have this global method to return the project name:

    /// <summary>
    /// SQL Server database name of the currently selected project.
    /// This name is merged into the connection string in EventConnectionString.
    /// </summary>
    public static string ProjectName
    {
        get
        {
            String _ProjectName = null;
            // See if we have it already
            if (HttpContext.Current.Items["ProjectName"] != null)
            {
                _ProjectName = (String)HttpContext.Current.Items["ProjectName"];
            }
            // Only have to do this once in each request
            if (String.IsNullOrEmpty(_ProjectName))
            {
                // Do we have it in the authentication ticket?
                if (HttpContext.Current.User != null)
                {
                    if (HttpContext.Current.User.Identity.IsAuthenticated)
                    {
                        if (HttpContext.Current.User.Identity is FormsIdentity)
                        {
                            FormsIdentity identity = (FormsIdentity)HttpContext.Current.User.Identity;
                            FormsAuthenticationTicket ticket = identity.Ticket;
                            _ProjectName = ticket.UserData;
                        }
                    }
                }
                // Do we have it in the session (user not logged in yet)
                if (String.IsNullOrEmpty(_ProjectName))
                {
                    if (HttpContext.Current.Session != null)
                    {
                        _ProjectName = (string)HttpContext.Current.Session["ProjectName"];
                    }
                }
                // Default to the test project
                if (String.IsNullOrEmpty(_ProjectName))
                {
                    _ProjectName = "Test_Project";
                }
                // Place it in current items so we do not have to figure it out again
                HttpContext.Current.Items["ProjectName"] = _ProjectName;
            }
            return _ProjectName;
        }
        set 
        {
            HttpContext.Current.Items["ProjectName"] = value;
            if (HttpContext.Current.Session != null)
            {
                HttpContext.Current.Session["ProjectName"] = value;
            }
        }
    }
Etherman
  • 1,777
  • 1
  • 21
  • 34
1

Can't you postback the project selection to some page, add that selection to the session, then redirect to appropriate protected page, where auth will kick in and force login?

ASP.NET session doesn't get created in the form of a cookie until you place at least one item in it.

  • Using Session[] does not work because if we add a role restriction to the web.config file for a certain page, the role provider is triggered to determine the role of the current user before the Session[] data has been determined (HttpContext.Current.Sessions is null during the call to GetRolesForUser()). However, I am able to use cookies to store the data and retrieve it during GetRolesForUser(). – Etherman Feb 01 '12 at 07:26
  • I thought you said the dropdown was on anon page - which means that you don't know who the user is, let alone their role. But, I'm glad you figured it out... –  Feb 01 '12 at 07:33
  • That is correct - the project selection is independant of user logon, so an anonymous user may already have selected the project (which I stick in a cookie). When we go to a page that requires a certain role (authorised user who is now logged on) I discovered that the question of whether or not the user belongs to this required role (call to GetRolesForUser) ocurrs in the page lifecycle BEFORE ASP.NET has populated the Session[] data (Session is null). Hence, I can use a cookie but not a session var. – Etherman Feb 01 '12 at 07:44
  • You can, or you can store that value to session on an intermediate anon page (or on the same page where the dropdown is, during a postback, which would then redirect). Anyways, same thing - the point is to get it working; there is little difference in the how. –  Feb 01 '12 at 07:56