Sunday, May 1, 2011

How might a site like Stack Overflow pass user information around in ASP.NET MVC?

Basically, I log into my website using OpenId, very similar to what I am assuming SO does. When I get the information back, I throw it into a database and create my "Registered User". I set my AuthCookie:

FormsAuthentication.SetAuthCookie(user.Profile.MyProfile.DisplayName, false);

Then I can use this for the User Name. However, I would like to pass in the entire object instead of just the string for display name. So my question is:

How does SO do it?

Do they extend/override the SetAuthCookie(string, bool) method to accept the User object, i.e. SetAuthCookie(User(object), bool).

What is the best way to persist a User object so that it is available to my UserControl on every single page of my Web Application?

Thanks in advance!

From stackoverflow
  • You can achieve this behavior by implementing your custom Membership Provider, or extending an existing one. The provider stores user information based on a key (or just by user name) and provides access to the MembershipUser class, which you can extend however you wish. So when you call FormsAuthentication.SetAuthCookie(...), you basically set the user key, which can be accessed be the provider.

    When you call Membership.GetUser(), the membership infrastructure will invoke the underlying provider and call its GetUser(...) method providing it with a key of the current user. Thus you will receive the current user object.

  • One way is to inject into your controller a class that is responsible for retrieving information for the current logged in user. Here is how I did it. I created a class called WebUserSession which implements an interface called IUserSession. Then I just use dependency injection to inject it into the controller when the controller instance is created. I implemented a method on my interface called, GetCurrentUser which will return a User object that I can then use in my actions if needed, by passing it to the view.

    using System.Security.Principal;
    using System.Web;
    
    public interface IUserSession
    {
        User GetCurrentUser();
    }
    
    public class WebUserSession : IUserSession
    {
        public User GetCurrentUser()
        {
            IIdentity identity = HttpContext.Current.User.Identity;
            if (!identity.IsAuthenticated)
            {
                return null;
            }
    
            User currentUser = // logic to grab user by identity.Name;
            return currentUser;
        }
    }
    
    public class SomeController : Controller
    {
        private readonly IUserSession _userSession;
    
        public SomeController(IUserSession userSession)
        {
            _userSession = userSession;
        }
    
        public ActionResult Index()
        {
            User user = _userSession.GetCurrentUser();
            return View(user);
        }
    }
    

    As you can see, you will now have access to retrieve the user if needed. Of course you can change the GetCurrentUser method to first look into the session or some other means if you want to, so you're not going to the database all the time.

  • Jeff,

    As I said in a comment to your question above, you must use the ClaimedIdentifier for the username -- that is, the first parameter to SetAuthCookie. There is a huge security reason for this. Feel free to start a thread on dotnetopenid@googlegroups.com if you'd like to understand more about the reasons.

    Now regarding your question about an entire user object... if you wanted to send that down as a cookie, you'd have to serialize your user object as a string, then you'd HAVE TO sign it in some way to protect against user tampering. You might also want to encrypt it. Blah blah, it's a lot of work, and you'd end up with a large cookie going back and forth with every web request which you don't want.

    What I do on my apps to solve the problem you state is add a static property to my Global.asax.cs file called CurrentUser. Like this:

    public static User CurrentUser {
        get {
            User user = HttpContext.Current.Items["CurrentUser"] as User;
            if (user == null && HttpContext.Current.User.Identity.IsAuthenticated) {
                user = Database.LookupUserByClaimedIdentifier(HttpContext.Current.User.Identity.Name);
                HttpContext.Current.Items["CurrentUser"] = user;
            }
            return user;
        }
    }
    

    Notice I cache the result in the HttpContext.Current.Items dictionary, which is specific to a single HTTP request, and keeps the user fetch down to a single hit -- and only fetches it the first time if a page actually wants the CurrentUser information.

    So a page can easily get current logged in user information like this:

    User user = Global.CurrentUser;
    if (user != null) { // unnecessary check if this is a page that users must be authenticated to access
        int age = user.Age; // whatever you need here
    }
    
    Jeff Ancel : I am going to look into this. I spent 8 hours yesterday making my own MembershipProvider. In a few hours I will update. This way is much easier than the way I did it yesterday. I also like what I see here.
    Jeff Ancel : I think that this is what I am going to go with. It plugs right in to what I have and only requires the one call like you mention. I very well could do this without the custom provider at all using the method you described. Thanks.
    Andrew Arnott : Glad it will work for you. I find that the asp.net membership provider is a very poor fit for redirect-based, passwordless authentication protocols such as OpenID. Any membership provider that works with OpenID can only be implemented half-way and hokey at best due to the interface that just doesn't fit very well.

0 comments:

Post a Comment