Monday, March 26, 2012

Ajax authentication, custom membership; cannot use Session object

I'm using ajax 1.0 authentication but based on a custom sql membership service which is working 100% except that in the membership code the Session object (HttpContext.Current.Session) cannot be accessed which though is crucial because I user Session["UsersID"] for login based functions. Any ideas how this can be fixed?

Hello,

Would you give me more details (when, where you can't access session object) ?

I don't have any problems with Session object even with Ajax async call back??

May be your situation is different from mine!


yes is in the authentication function in my custom membership provider code;

publicoverridebool ValidateUser(string username,string password)

{

SqlConnection conn =newSqlConnection(connectionString);SqlCommand cmd =newSqlCommand("ValidateUser", conn);

cmd.CommandType =

CommandType.StoredProcedure;

cmd.Parameters.Add(

"@.Username",SqlDbType.VarChar, 12).Value = username;

cmd.Parameters.Add(

"@.Password",SqlDbType.VarChar, 12).Value = password;

cmd.Parameters.Add(

"@.IPNumber",SqlDbType.VarChar, 16).Value =HttpContext.Current.Request.UserHostAddress;SqlDataReader reader =null;try

{

conn.Open();

reader = cmd.ExecuteReader(

CommandBehavior.SingleRow);if (reader.Read() ==false)

{

returnfalse;

}

int _usersID =Convert.ToInt32(reader["UsersID"]);

HttpContext.Current.Session["UsersID"] = _usersID;

}

...


Hi Dabbi2000,

That's indeed very tempting to do. However, this construct will never work. The deal is that your code is executed by an http module that runs before the AcquireRequestState event. In other words: there is no session yet.

The best way to achieve what you want is to store your user id in the items collection of the current context and than move it from context to session in the PostAcquireRequestState event. So replace the last line of your example with:

HttpContext.Current.Items["UsersID"] = _usersID;

and then after the session state has been loaded (for example in PostAcquireRequestState)

HttpContext.Current.Session["UsersID"] = HttpContext.Current.Items["" UsersID"];
HttpContext.Current.Items.Remove("UsersID");

HTH,

-- Henkk


Henk,

that is an excellent solution! Unfortunately the Items container doesn't contain the UsersID when accessing from PostAcquireRequestState event. Using VS Debugger and Fiddler it simply seems to be lost somewhere between the authentication code and the PostAcquireRequestState event (the requesting assembly is "~/Authentication_JSON_AppService.axd"). Still there are other system variables in the Item like Is there some obvious explanation for that??


hmm not entirely correct, it isn't lost but what happens is that json authentication causes PostAcquireRequestState event BEFORE the membership validateuser() function is called?!!


Hi Dabbi2000,

I wanted to see for myself and, yes, you're right. What's going on ? In a normal postback (including async ones) the pipeline follows the chain of events I described in a previous post.

The problem is: I thought you were using the normal forms based authentication stuff in combination with an Ajax enabled app. However, you appear to be using the Sys.Services.AuthenticationService. Unfortunately (my bad, sorry) this is not a normal postback. It's uses the same infrastructure (ScriptHandler) as script services (as far as I can see, it is a script service dressed up as a resource). This means that indeed by the time you reach the endpoint (the authentication service backend) the PostAcquireSessionState event is long gone.

Fortunately, there are other events to subscribe to. But, unfortunately, if the PostAcquireSessionState is done and there is no session state, they most have turned sessions off for this service (and indeed the Session property in the HttpContext returns null).

Meaning, there is no way for you to use the Session object.

However, if the only thing you want to do is get to the current user id, the FormsAuthenticationModule (like all other builtin authentication modules) makes sure the currently logged on user is reflected in the Page.User property (or HttpContext.User for which Page.User is just a wrapper).

If you need to store more, the profile should still work. BUT only after you made a roundtrip to the authentication service and you successfuly authenticated. The authentication service calls into the FormsAuthentication class to perform authentication and returns an authentication cookie like normal, but unfortunately it forgets to set HttpContext.User like the builtin authentication modules would do (feature request ...). This means the profile module has no info to fly on and will do an anonymous profile while in the call chain of the authentication service.

HTH,

-- Henkk


I wrote a blog post about the problem:http://www.jeffzon.net/Blog/post/Session-enabled-authentication-service.aspx

The only thing we should do is to use the following authentication service class instead of the default one:

using System;using System.Web;using System.Web.Services;using System.Web.Services.Protocols;using System.Reflection;using System.Web.Script.Services;using System.Web.UI;using System.Web.Security;[WebService(Namespace ="http://tempuri.org/")][WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)][ScriptService]public class SessionEnabledAuthService : System.Web.Services.WebService{private static MethodInfo checkEnabledMethodInfo;static SessionEnabledAuthService() { Type authServiceType =typeof(ScriptManager).Assembly.GetType("System.Web.Security.AuthenticationService"); checkEnabledMethodInfo = authServiceType.GetMethod("CheckAuthenticationServicesEnabled", BindingFlags.NonPublic | BindingFlags.Static); }private static void CheckAuthenticationServicesEnabled(bool enforceSSL) { checkEnabledMethodInfo.Invoke(null,new object[] { HttpContext.Current, enforceSSL }); } [WebMethod(EnableSession =true)] [ScriptMethod]public bool Login(string userName,string password,bool createPersistentCookie) { CheckAuthenticationServicesEnabled(true);if (Membership.Provider.ValidateUser(userName, password)) { FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);return true; }return false; } [WebMethod(EnableSession =true)] [ScriptMethod]public void Logout() { CheckAuthenticationServicesEnabled(false); FormsAuthentication.SignOut(); }}

Hi Jeff,

This of course would work, but a couple of remarks:

1) namespacehttp://tempuri.org/ is not very unique ;-) you'll want to change this into something that makes more sense

2) I wouldn't mark this service as basic profile compliant. In normal situations it doesn't even do SOAP (script services serialize JSON by default). And even when you call it using a normal proxy, it's main result is returned as a cookie (which is not basic profile compliant, although I don't think the builtin pipeline check catches that).

3) By using reflection to call into CheckAuthenticationServicesEnabled you now have to run full trust (you'll need SecurityPermissionFlags.UnmanagedCode). Which means you either have to run the webservice full trust, or you'll have to install an separate assembly in the GAC (or grant it full trust with a custom policy). You'd be better of implementing this yourself, because the only really interesting thing CheckAuthenticationServicesEnabled does for you is check if SSL is enabled, which you can easilty do yourself with HttpRequest.IsSecureConnection (the other part is checking if an enabled flag is set on AuthenticationService itself, but since you're implementing your own, you already know it's enabled ...)

4) just as a word of warning for those who want to use this construct in production: I rarely think it's a good idea to repeat builtin functionality (with a minor change). I guess the team had good reasons to disable sessions (they are chatty things anyway) and there's plenty of workarounds. I do think it is too bad they didn't setup HttpContext.User and Thread.CurrentPrincipal like the pipeline authentication modules do, but alas. Regardless, the problem of getting to the current user is solved in an entirely different way, so the original poster will not benefit from this service. He can simply look an Page.User and be done with it.

-- Henkk


hello,

your discussions are a little more advanced than my programming knowledge :) but I would like to comment on that solution of using Page.User. User.Identity stores the username which is very unpractical. Imagine all the tables where I have to refer rows to user e.g. forum posts. User names can be whole names, and in my web would contain icelandic letters and so I think they are very bad for primary keys. That is the reason for why I use Session["UsersID"] instead of the User object.


D'uh. Don't know what I was thinking, but you'll need reflectionPermissionFlags.MemberAccess. What I said about SecurityPermissionFlags.UnmanagedCode is complete and utter BS. The net result is the same though. You'll need full trust ...

-- Henkk


Great feedback, Henkk, I totoally agree with you.

Actually I just gave a prototoype of the solution - or more likely a work around. In this situation, if the poster really need to use session state in authentication, he can use the service class I wrote - remove the validation method will be enough.


Hi,

The mapping from identity.name to user id can be done in a variety of ways. There is a very elegant one that sticks out I think (this is my personal preference BTW), which has to do with the way membership providers work. The builtin membership providers can do a mapping of the current user onto a so called MembershipUser (just a class in the library). The membership user contains more info than your average principal (including password questions, email addresses, stuff like that). You can get to the membership user for the current user by calling Membership.GetUser().

Since you implement your own provider you get to fill this information yourself (in the GetUser(userName, userIsOnline) override on your custom membership provider). One of the bits of info in a membership user is a ProviderUserKey. This is an object that you can fill with whatever info you want. In your case, probably the user id. The standard SqlMembershipProvider stores a GUID in it, BTW.

Whenever you need to get to the current user id, just use Membership.GetUser().ProviderUserKey.

Be aware though that the mapping is not cached, so if it is expensive to get from current user name to current user id, you better cache the mapping (in the ASP.NET cache ...).

The beauty of the construct is that now all custom logic is in your membership provider ...

HTH,

-- Henkk


hope you guys are still watching this thread...

Henk's solution is tempting... it's a bit expensive for only retrieving an Int value but if it survives the AJAX authentication I'm ok with it! I'm no good at asp.net cache, how would I use that for this situation??


that is, how to make it specific to each user, I thought the cache object scope was application only?

No comments:

Post a Comment