675

I need to do something fairly simple: in my ASP.NET MVC application, I want to set a custom IIdentity / IPrincipal. Whichever is easier / more suitable. I want to extend the default so that I can call something like User.Identity.Id and User.Identity.Role. Nothing fancy, just some extra properties.

I've read tons of articles and questions but I feel like I'm making it harder than it actually is. I thought it would be easy. If a user logs on, I want to set a custom IIdentity. So I thought, I will implement Application_PostAuthenticateRequest in my global.asax. However, that is called on every request, and I don't want to do a call to the database on every request which would request all the data from the database and put in a custom IPrincipal object. That also seems very unnecessary, slow, and in the wrong place (doing database calls there) but I could be wrong. Or where else would that data come from?

So I thought, whenever a user logs in, I can add some necessary variables in my session, which I add to the custom IIdentity in the Application_PostAuthenticateRequest event handler. However, my Context.Session is null there, so that is also not the way to go.

I've been working on this for a day now and I feel I'm missing something. This shouldn't be too hard to do, right? I'm also a bit confused by all the (semi)related stuff that comes with this. MembershipProvider, MembershipUser, RoleProvider, ProfileProvider, IPrincipal, IIdentity, FormsAuthentication.... Am I the only one who finds all this very confusing?

If someone could tell me a simple, elegant, and efficient solution to store some extra data on a IIdentity without all the extra fuzz.. that would be great! I know there are similar questions on SO but if the answer I need is in there, I must've overlooked.

Kiquenet
  • 14,494
  • 35
  • 148
  • 243
Razzie
  • 30,834
  • 11
  • 63
  • 78
  • how did you integrate this with the pre-written MVC code for the login? – Stefanvds Feb 16 '11 at 15:51
  • stefan, you don't have to chance a lot regarding the existing AccountController. The trick really is to set the cookie in the global.asax, and you only have to write some data to the formsauthentication cookie yourself after you login in the AccountController. You can use the FormsAuthenticationTicket for that, which you can pass custom data. – Razzie Feb 28 '11 at 09:55
  • Hi, Razzie. I have a question: You retrieve data from FormsAuthenticationTicket without DB, and you can update the ticket(actually is cookie) when user update his profile, so everything is fine. However, if user change his data in another place, your data(retrive from cookie) in previous place is invalid. How did you handle the issue? – Domi.Zhang Jan 28 '12 at 09:35
  • 1
    Hi Domi, it's a combination of only storing data that never changes (like a user ID) or updating the cookie directly after the user changes data that has to be reflected in the cookie right away. If a user does that, I simply update the cookie with the new data. But I try not to store data that changes often. – Razzie Jan 28 '12 at 16:26
  • 28
    this question has 36k views and many upvotes. is this really that common a requirement - and if so isn't there a better way than all this 'custom stuff'? – Simon_Weaver Feb 06 '13 at 12:29
  • 2
    @Simon_Weaver There is ASP.NET Identity know, which supports additional custom information in the encrypted cookie more easily. – John Apr 10 '15 at 11:04
  • 2
    I agree with you, there is to much information like you posted: `MemberShip...`, `Principal`, `Identity`. ASP.NET should make this easier, simpler and at most two approaches for dealing with authentication. – broadband Aug 12 '16 at 16:15
  • 1
    @Simon_Weaver This clearly shows there's demand for simpler easier more flexible identity system IMHO. – niico Apr 15 '17 at 19:08
  • 1
    @broadband I hear you.. they don't half make it hard work for people new to this stuff – Murphybro2 Sep 01 '17 at 08:25

9 Answers9

862

Here's how I do it.

I decided to use IPrincipal instead of IIdentity because it means I don't have to implement both IIdentity and IPrincipal.

  1. Create the interface

    interface ICustomPrincipal : IPrincipal
    {
        int Id { get; set; }
        string FirstName { get; set; }
        string LastName { get; set; }
    }
    
  2. CustomPrincipal

    public class CustomPrincipal : ICustomPrincipal
    {
        public IIdentity Identity { get; private set; }
        public bool IsInRole(string role) { return false; }
    
        public CustomPrincipal(string email)
        {
            this.Identity = new GenericIdentity(email);
        }
    
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  3. CustomPrincipalSerializeModel - for serializing custom information into userdata field in FormsAuthenticationTicket object.

    public class CustomPrincipalSerializeModel
    {
        public int Id { get; set; }
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }
    
  4. LogIn method - setting up a cookie with custom information

    if (Membership.ValidateUser(viewModel.Email, viewModel.Password))
    {
        var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First();
    
        CustomPrincipalSerializeModel serializeModel = new CustomPrincipalSerializeModel();
        serializeModel.Id = user.Id;
        serializeModel.FirstName = user.FirstName;
        serializeModel.LastName = user.LastName;
    
        JavaScriptSerializer serializer = new JavaScriptSerializer();
    
        string userData = serializer.Serialize(serializeModel);
    
        FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                 1,
                 viewModel.Email,
                 DateTime.Now,
                 DateTime.Now.AddMinutes(15),
                 false,
                 userData);
    
        string encTicket = FormsAuthentication.Encrypt(authTicket);
        HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
        Response.Cookies.Add(faCookie);
    
        return RedirectToAction("Index", "Home");
    }
    
  5. Global.asax.cs - Reading cookie and replacing HttpContext.User object, this is done by overriding PostAuthenticateRequest

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];
    
        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
    
            JavaScriptSerializer serializer = new JavaScriptSerializer();
    
            CustomPrincipalSerializeModel serializeModel = serializer.Deserialize<CustomPrincipalSerializeModel>(authTicket.UserData);
    
            CustomPrincipal newUser = new CustomPrincipal(authTicket.Name);
            newUser.Id = serializeModel.Id;
            newUser.FirstName = serializeModel.FirstName;
            newUser.LastName = serializeModel.LastName;
    
            HttpContext.Current.User = newUser;
        }
    }
    
  6. Access in Razor views

    @((User as CustomPrincipal).Id)
    @((User as CustomPrincipal).FirstName)
    @((User as CustomPrincipal).LastName)
    

and in code:

    (User as CustomPrincipal).Id
    (User as CustomPrincipal).FirstName
    (User as CustomPrincipal).LastName

I think the code is self-explanatory. If it isn't, let me know.

Additionally to make the access even easier you can create a base controller and override the returned User object (HttpContext.User):

public class BaseController : Controller
{
    protected virtual new CustomPrincipal User
    {
        get { return HttpContext.User as CustomPrincipal; }
    }
}

and then, for each controller:

public class AccountController : BaseController
{
    // ...
}

which will allow you to access custom fields in code like this:

User.Id
User.FirstName
User.LastName

But this will not work inside views. For that you would need to create a custom WebViewPage implementation:

public abstract class BaseViewPage : WebViewPage
{
    public virtual new CustomPrincipal User
    {
        get { return base.User as CustomPrincipal; }
    }
}

public abstract class BaseViewPage<TModel> : WebViewPage<TModel>
{
    public virtual new CustomPrincipal User
    {
        get { return base.User as CustomPrincipal; }
    }
}

Make it a default page type in Views/web.config:

<pages pageBaseType="Your.Namespace.BaseViewPage">
  <namespaces>
    <add namespace="System.Web.Mvc" />
    <add namespace="System.Web.Mvc.Ajax" />
    <add namespace="System.Web.Mvc.Html" />
    <add namespace="System.Web.Routing" />
  </namespaces>
</pages>

and in views, you can access it like this:

@User.FirstName
@User.LastName
Undo
  • 25,519
  • 37
  • 106
  • 129
LukeP
  • 10,422
  • 6
  • 29
  • 48
  • 9
    Nice implementation; watch out for RoleManagerModule replacing your custom principal with a RolePrincipal. That caused me a lot of pain - http://stackoverflow.com/questions/10742259/using-custom-iprincipal-and-iidentity-in-mvc3 – David Keaveny Jun 26 '12 at 07:29
  • Lovely, I just have one question: Can storing the user id in the forms auth ticket, as you've just described cause any security issues? Is it possible that the user can tamper that data? – Adam Vigh Oct 03 '12 at 11:55
  • 1
    @AdamVigh See here: http://support.microsoft.com/kb/910443 (What is the role of a ticket in Forms Authentication?) and here: http://stackoverflow.com/questions/8635965/asp-net-forms-authentication-ticket-tampered-but-still-works - so it looks like a tampered with ticket will not decrypt unless it's a situation described in the second link (you're replacing hex 0 with G-Z letters which will be converted to 0 anyway therefore keeping the ticket intact). – LukeP Oct 03 '12 at 17:54
  • 1
    How would you manage an anonymous user? The code in `Application_PostAuthenticateRequest` won't run and the `as` cast will return null everywhere. Normally, the `User` property is never null. – Pierre-Alain Vigeant Nov 20 '12 at 21:47
  • 9
    ok I found the solution, just add an else switch which pass "" (empty string) as the email and the Identity will be anonymous. – Pierre-Alain Vigeant Nov 20 '12 at 22:27
  • 3
    DateTime.Now.AddMinutes(N)... how to make this so it doesn't logout user after N minutes, can the logged in user be persisted (when user check 'Remember Me' for example)? – 1110 Dec 25 '12 at 13:00
  • @LukeP: Perfect solution! and even integrated with MVC! Just design a question, which directory/project do you usually put the ICustomPrincipal/CustomPrincipal files? – Gabriel Jan 10 '13 at 11:04
  • @Gabriel I put all membership code in the main project. I will usually create a Membership folder/namespace reference it where it's needed. – LukeP Jan 10 '13 at 16:13
  • 2
    Isn't this a bit unsecure if our principal has sensitive data such has roles or permissions? – Rui Lima Mar 21 '13 at 18:59
  • This is a great answer. I am having one problem with though. My session/cookie doesn't persist between browser sessions. Has anyone else had this issue or know a fix? – Ben Cameron Apr 10 '13 at 21:22
  • Have you tried making your FormsAuthenticationTicket object persistent (by changing false to true)? – LukeP Apr 11 '13 at 09:02
  • @LukeP - does the `Thread.CurrentPrincipal` also need to be set to the `newUser` in the `PostAuthenticationRequest`? (See comment from Rus Cam in answer from Sriwantha Attanayake.) – mg1075 May 12 '13 at 00:29
  • what is the user repository class in this line? var user = userRepository.Users.Where(u => u.Email == viewModel.Email).First(); – hakan Jun 16 '13 at 23:11
  • 1
    @piedpiper It's whatever you use to get your user object from your data layer. This is where you would get user's information like first name and last name from. – LukeP Jun 17 '13 at 00:27
  • FYI - If anyone is still having trouble with the principal being null for anonymous users, this q/a solved the problem for me - http://stackoverflow.com/questions/11476301/how-to-create-a-customprincipal-globally-with-and-without-authorizeattribute – ryanulit Jun 27 '13 at 19:10
  • instead of manually using the **as** conversion repeatedly you can use an extension method: `public static ToCustom(this IPrincipal principal) { return principal as ; }` `@User.ToCustom().someProperty` – TugboatCaptain Jul 15 '13 at 18:35
  • 4
    If you are using the WebApiController, you will need to set `Thread.CurrentPrincipal` at `Application_PostAuthenticateRequest` for it to work as it does not rely on `HttpContext.Current.User` – Jonathan Levison Jul 16 '13 at 19:50
  • 1
    The `FormsAuthentication` cookie should have the `HttpOnly` attribute set to true to prevent JavaScript from accessing your auth cookie. I also had to put the `PostAuthenticateRequest` event wireup in `public override void Init()` otherwise I got a `NullReferenceException` from the framework. But the answer was great other than that :) – Mike Wade Jul 26 '13 at 13:57
  • Has anyone figured out how to accomplish log off with this ? Logoff seems to have broken. – Abhinav Gujjar Jan 29 '14 at 19:01
  • 3
    @AbhinavGujjar `FormsAuthentication.SignOut();` works fine for me. – LukeP Jan 29 '14 at 21:30
  • Any suggestions on how to do this with OWIN? – BlackICE Feb 04 '14 at 21:18
  • 1
    @BlackICE I'm currently writing an app that uses OWIN and I do it a bit differently now. I have a need to store a lot more information than I am prepared to in a cookie so I just store email address in a claim and create work context with full user object pulled from cache, or database if it's not in cache yet. Ask a question and link it here and I'll do my best to help. – LukeP Feb 04 '14 at 21:28
  • @LukeP I'm reading up on OWIN now to get up to speed on it, I don't think a cookie is the best place for what I'm storing either as it could be substantial. Is Cache better than Session for this? – BlackICE Feb 07 '14 at 15:08
  • @BlackICE I don't think one is better than the other one. They're different. I like using cache because I've already implemented a nice wrapper for all my cache needs. Also see: http://stackoverflow.com/questions/428634/advantages-of-cache-vs-session – LukeP Feb 08 '14 at 17:38
  • @LukeP Here's my question: http://stackoverflow.com/questions/21679836/custom-identity-using-mvc5-and-owin – BlackICE Feb 10 '14 at 14:39
  • @BlackICE I'll answer when I get back home from work. Like I said I am doing it differently now so you might or might not like it, but it will let you do what you want to do. – LukeP Feb 10 '14 at 15:33
  • Great answer! If by any chance you want to serialize DateTime's, then i would suggest not to use JavaScriptSerializer. It converts the date to a format like so: /Date(352162800000)/, and when deserialising i ended up with a date a day earlier...(timezone glitch maybe?) i would recomend NewtonSoft's Json.NET (http://james.newtonking.com/json) to do the serializing. – Mark Homans Feb 20 '14 at 15:02
  • 1
    Could you add some more and lighten us by explaining adding roles to this method and using it in controller "Users = "Admin" ? – Sakthivel Jun 26 '14 at 11:29
  • I always get null from Request.Cookies[FormsAuthentication.FormsCookieName], it's weird. – Timeless Jul 26 '14 at 23:03
  • @Timeless Try using Fiddler and checking if the cookie is being set correctly during login. – LukeP Jul 27 '14 at 12:08
  • @LukeP Is there any limitation on the length(size) of the userdata? – Timeless Jul 28 '14 at 05:02
  • @Timeless There are limits to how big the cookie can be. See here: http://stackoverflow.com/questions/5381526/what-are-the-current-cookie-limits-in-modern-browsers – LukeP Jul 28 '14 at 17:29
  • At least in MVC 5 you need to do `Context.User = newUser;` instead of `HttpContext.Current.User = newuser`. – Pharylon Sep 17 '14 at 18:27
  • 1
    @LukeP I am not able to use in the view like `@User.FirstName` as you have mentioned. I have followed your post exactly, Is there anything that needs to be set? `@((User as CustomPrincipal).FirstName)` is working – Rajshekar Reddy Mar 10 '15 at 15:37
  • @LukeP same question as Reddy. I am not able to use in the view like '@User.FirstName' as you have mentioned. I have followed your post exactly, Is there anything that needs to be set? '@((User as CustomPrincipal).FirstName)' is working – crichavin Mar 10 '15 at 23:51
  • @Reddy and Chad Richardson: Have you implemented the BaseViewPage and made it default in Views/web.config? Also which version of MVC are you using? – LukeP Mar 23 '15 at 17:29
  • @LukeP sorry I had to set the `` in the webConfig of the views folder but I did it in the main wenConfig. My bad I overlooked it. All working fine. Thanks Brother – Rajshekar Reddy Mar 24 '15 at 11:24
  • 1
    @LukeP how do `Roles` fit into the above? When I put this `[Authorize(Roles = "Admin")]`on my controller, it keeps failing, even though my user is in Role Admin (I checked Db) – J86 Apr 02 '15 at 11:24
  • @Ciwan check the the principal to make sure it's getting loaded in. I can't speak for what you're using, but we use ClaimsPrincipal. As long as the type and value are set correctly, it should work. In some cases, you may need a custom authorize attribute depending on how you implemented the identity. – Sinaesthetic Apr 28 '15 at 00:37
  • @LukeP - this probably needs a question all it's own, but how do I use my own custom password authentication? I have an existing ASP.NET app that I'm porting to MVC5. It seems easiest if I can keep using my existing User table with encrypted passwords. `Hash hash = new Hash("SHA256"); if(u.Password == hash.Encrypt(EnteredTyped))` And not have to switch everything over to Membership? Or how do extend Membership with custom Authentication that will allow me to point it to my existing User table? – Ryan Vettese May 08 '15 at 21:36
  • thanks for the solution, after using this; how can I use the authentication for a specific controller?? – Ehsan Jul 02 '15 at 22:21
  • @LukeP I implement Custom IPrinciple as you mention above, but I am getting `authCookie = null` always, does I am missing anything ? – gaurav bhavsar Sep 22 '15 at 09:00
  • 1
    Is there any new/improved way of implementation for MVC5? – fiberOptics Dec 09 '15 at 13:29
  • How to manage an anonymous user `else { HttpContext.Current.User = new CustomPrincipal(""); } after if (authCookie != null) { //.. }` Unfortunately I have no idea if is it safe. – Pcodea Xonos Feb 10 '16 at 09:37
  • @LukeP, Your step-4 login method not contain the returnUrl,with out using the [Authorize] attribute how to get return url. – shamim May 18 '16 at 13:07
  • I've been developing in C#/.NET for quite a while now but recently got into web. I didn't get into authentication/authorization until today and I started by saying that I didn't want to just use the built in stuff by Microsoft. Your answer + debugging helped me understand the authentication model for ASP.NET. Thank you! – Joakim Hansson May 19 '16 at 16:52
  • I've been implementing it and it works fine on my local iis. When I host it on a medium trust server it expires my authentication less than 1 minuto. Is there something I can do? – Felipe Oriani Oct 06 '16 at 00:47
  • @LukeP this is a great solution. The only downside is - it works with FormsAuth only. I need to be able to use both Forms and Windows authentication. (the `WindowsPricncipal` one). Will keep digging. – Alex from Jitbit Jun 08 '17 at 18:52
  • Why does "IsInRole" always return false in your IPrincipal? – Mike Jul 30 '18 at 19:41
  • @Mike Because it's just an example and not full implementation. Outside of the scope of the question. – LukeP Jul 31 '18 at 14:45
  • If I was to use the code as is then I would never get past the login page to an Authorized page. The IsInRole will get called return false and take action to have the user login in again. in your code example you should probably return true and add a comment to show that more work is needed in that area to ensure proper authentication. – Mike Jul 31 '18 at 17:23
  • thank u so much. but i have problem. sometimes (User as customprincipal) returm null. User is not null but (User as customprincipal) return null !!!!! – Reza Nabiloo Jan 19 '22 at 12:57
  • Works like a charm. Fascinating that Microsoft didn't implement this. – ojonasplima Dec 20 '22 at 12:54
111

I can't speak directly for ASP.NET MVC, but for ASP.NET Web Forms, the trick is to create a FormsAuthenticationTicket and encrypt it into a cookie once the user has been authenticated. This way, you only have to call the database once (or AD or whatever you are using to perform your authentication), and each subsequent request will authenticate based on the ticket stored in the cookie.

A good article on this: http://www.ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html (broken link)

Edit:

Since the link above is broken, I would recommend LukeP's solution in his answer above: https://stackoverflow.com/a/10524305 - I would also suggest that the accepted answer be changed to that one.

Edit 2: An alternative for the broken link: https://web.archive.org/web/20120422011422/http://ondotnet.com/pub/a/dotnet/2004/02/02/effectiveformsauth.html

Community
  • 1
  • 1
John Rasch
  • 62,489
  • 19
  • 106
  • 139
  • Coming from PHP, I've always put the information like UserID and other pieces needed to grant restricted access in Session. Storing it client-side makes me nervous, can you comment on why that won't be a problem? – John Zumbrum Apr 24 '12 at 12:54
  • @JohnZ - the ticket itself is encrypted on the server before it's sent over the wire, so it's not like the client is going to have access to the data stored within the ticket. Note that session IDs are stored in a cookie as well, so it's not really all that different. – John Rasch Apr 24 '12 at 14:42
  • @JohnZ - Session is also a cookie saved on the client side. – Guillermo Gutiérrez Aug 28 '12 at 17:29
  • 3
    If you are here you should look at LukeP's solution – mynkow Apr 26 '13 at 07:59
  • 2
    I've always been concerned with the potential for exceeding the maximum cookie size (http://stackoverflow.com/questions/8706924/how-big-of-a-cookie-can-should-i-create) with this approach. I tend to use the `Cache` as a `Session` replacement to keep the data on the server. Can anyone tell me if this is a flawed approach? – Red Taz May 08 '13 at 15:14
  • 2
    Nice approach. One potential problem with this is if your user object has more than a few properties (and especially if any nested objects), creating the cookie will fail silently once the encrypted value is over 4KB (much easier to hit then you might think). If you only store key data it's fine but then you'd have to hit DB still for the rest. Another consideration is "upgrading" cookie data when the user object has signature or logic changes. – Geoffrey Hudik Jun 05 '13 at 01:36
  • Joe Stagner addresses the question of cookie vs. cache in his excellent video [Use Custom Principal Objects](http://www.asp.net/web-forms/videos/authentication/use-custom-principal-objects). The examples are in Web Forms, but this was still the best 20 minutes I spent while working on FormsAuthentication. – zacharydl Aug 08 '13 at 03:09
64

Here is an example to get the job done. bool isValid is set by looking at some data store (lets say your user data base). UserID is just an ID i am maintaining. You can add aditional information like email address to user data.

protected void btnLogin_Click(object sender, EventArgs e)
{         
    //Hard Coded for the moment
    bool isValid=true;
    if (isValid) 
    {
         string userData = String.Empty;
         userData = userData + "UserID=" + userID;
         FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, username, DateTime.Now, DateTime.Now.AddMinutes(30), true, userData);
         string encTicket = FormsAuthentication.Encrypt(ticket);
         HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
         Response.Cookies.Add(faCookie);
         //And send the user where they were heading
         string redirectUrl = FormsAuthentication.GetRedirectUrl(username, false);
         Response.Redirect(redirectUrl);
     }
}

in the golbal asax add the following code to retrive your information

protected void Application_AuthenticateRequest(Object sender, EventArgs e)
{
    HttpCookie authCookie = Request.Cookies[
             FormsAuthentication.FormsCookieName];
    if(authCookie != null)
    {
        //Extract the forms authentication cookie
        FormsAuthenticationTicket authTicket = 
               FormsAuthentication.Decrypt(authCookie.Value);
        // Create an Identity object
        //CustomIdentity implements System.Web.Security.IIdentity
        CustomIdentity id = GetUserIdentity(authTicket.Name);
        //CustomPrincipal implements System.Web.Security.IPrincipal
        CustomPrincipal newUser = new CustomPrincipal();
        Context.User = newUser;
    }
}

When you are going to use the information later, you can access your custom principal as follows.

(CustomPrincipal)this.User
or 
(CustomPrincipal)this.Context.User

this will allow you to access custom user information.

Mrchief
  • 75,126
  • 20
  • 142
  • 189
Sriwantha Attanayake
  • 7,694
  • 5
  • 42
  • 44
16

MVC provides you with the OnAuthorize method that hangs from your controller classes. Or, you could use a custom action filter to perform authorization. MVC makes it pretty easy to do. I posted a blog post about this here. http://www.bradygaster.com/post/custom-authentication-with-mvc-3.0

Robert Westerlund
  • 4,750
  • 1
  • 20
  • 32
brady gaster
  • 1,506
  • 1
  • 10
  • 15
  • But session can be lost and user still authenticate. No ? – Dragouf Aug 18 '11 at 12:46
  • @brady gaster, I read your blog post(thanks!), Why would someone use the override "OnAuthorize()" as mentioned on your post over the global.asax entry "...AuthenticateRequest(..)" mentioned by the other answers? Is one preferred over the other in setting the principle user? – RayLoveless Apr 11 '16 at 18:01
11

Here is a solution if you need to hook up some methods to @User for use in your views. No solution for any serious membership customization, but if the original question was needed for views alone then this perhaps would be enough. The below was used for checking a variable returned from a authorizefilter, used to verify if some links wehere to be presented or not(not for any kind of authorization logic or access granting).

using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Security.Principal;

    namespace SomeSite.Web.Helpers
    {
        public static class UserHelpers
        {
            public static bool IsEditor(this IPrincipal user)
            {
                return null; //Do some stuff
            }
        }
    }

Then just add a reference in the areas web.config, and call it like below in the view.

@User.IsEditor()
Base
  • 1,061
  • 1
  • 11
  • 27
  • 1
    In your solution, We again need to do database calls every time. Because user object doesn't have custom properties. It only has Name and IsAuthanticated – oneNiceFriend Jun 05 '16 at 08:36
  • That depends entirely on your implementation and desired behavior. My sample contains 0 lines of database, or role, logic. If one use the IsInRole it could in turn be cached in cookie i believe. Or you implement your own caching logic. – Base Jun 05 '16 at 14:03
3

Based on LukeP's answer, and add some methods to setup timeout and requireSSL cooperated with Web.config.

The references links

Modified Codes of LukeP

1, Set timeout based on Web.Config. The FormsAuthentication.Timeout will get the timeout value, which is defined in web.config. I wrapped the followings to be a function, which return a ticket back.

int version = 1;
DateTime now = DateTime.Now;

// respect to the `timeout` in Web.config.
TimeSpan timeout = FormsAuthentication.Timeout;
DateTime expire = now.Add(timeout);
bool isPersist = false;

FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
     version,          
     name,
     now,
     expire,
     isPersist,
     userData);

2, Configure the cookie to be secure or not, based on the RequireSSL configuration.

HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
// respect to `RequreSSL` in `Web.Config`
bool bSSL = FormsAuthentication.RequireSSL;
faCookie.Secure = bSSL;
Community
  • 1
  • 1
AechoLiu
  • 17,522
  • 9
  • 100
  • 118
2

As an addition to LukeP code for Web Forms users (not MVC) if you want to simplify the access in the code behind of your pages, just add the code below to a base page and derive the base page in all your pages:

Public Overridable Shadows ReadOnly Property User() As CustomPrincipal
    Get
        Return DirectCast(MyBase.User, CustomPrincipal)
    End Get
End Property

So in your code behind you can simply access:

User.FirstName or User.LastName

What I'm missing in a Web Form scenario, is how to obtain the same behaviour in code not tied to the page, for example in httpmodules should I always add a cast in each class or is there a smarter way to obtain this?

Thanks for your answers and thank to LukeP since I used your examples as a base for my custom user (which now has User.Roles, User.Tasks, User.HasPath(int) , User.Settings.Timeout and many other nice things)

Alicia
  • 1,152
  • 1
  • 23
  • 41
Manight
  • 500
  • 5
  • 26
2

All right, so i'm a serious cryptkeeper here by dragging up this very old question, but there is a much simpler approach to this, which was touched on by @Baserz above. And that is to use a combination of C# Extension methods and caching (Do NOT use session).

In fact, Microsoft has already provided a number of such extensions in the Microsoft.AspNet.Identity.IdentityExtensions namespace. For instance, GetUserId() is an extension method that returns the user Id. There is also GetUserName() and FindFirstValue(), which returns claims based on the IPrincipal.

So you need only include the namespace, and then call User.Identity.GetUserName() to get the users name as configured by ASP.NET Identity.

I'm not certain if this is cached, since the older ASP.NET Identity is not open sourced, and I haven't bothered to reverse engineer it. However, if it's not then you can write your own extension method, that will cache this result for a specific amount of time.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • Why "do not use session"? – Alex from Jitbit Jun 08 '17 at 19:31
  • @jitbit - because session is unreliable, and insecure. For the same reason you should never use session for security purposes. – Erik Funkenbusch Jun 08 '17 at 19:42
  • "Unreliable" can be addressed by repopulating session (if empty). "Unsecure" - there are ways to protect from session hijacking (by useing HTTPS-only + other ways). But I actually agree with you. Where would you cache it then? Info like `IsUserAdministrator` or `UserEmail` etc.? You thinking `HttpRuntime.Cache`? – Alex from Jitbit Jun 08 '17 at 19:58
  • @jitbit - That's one option, or another cacheing solution if you have it. Making sure to expire the cache entry after a period of time. Insecure also applies to the local system, since you can manually alter the cookie and guess session ID's. Man in the middle is not the only concern. – Erik Funkenbusch Jun 08 '17 at 20:11
0

I tried the solution suggested by LukeP and found that it doesn't support the Authorize attribute. So, I modified it a bit.

public class UserExBusinessInfo
{
    public int BusinessID { get; set; }
    public string Name { get; set; }
}

public class UserExInfo
{
    public IEnumerable<UserExBusinessInfo> BusinessInfo { get; set; }
    public int? CurrentBusinessID { get; set; }
}

public class PrincipalEx : ClaimsPrincipal
{
    private readonly UserExInfo userExInfo;
    public UserExInfo UserExInfo => userExInfo;

    public PrincipalEx(IPrincipal baseModel, UserExInfo userExInfo)
        : base(baseModel)
    {
        this.userExInfo = userExInfo;
    }
}

public class PrincipalExSerializeModel
{
    public UserExInfo UserExInfo { get; set; }
}

public static class IPrincipalHelpers
{
    public static UserExInfo ExInfo(this IPrincipal @this) => (@this as PrincipalEx)?.UserExInfo;
}


    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginModel details, string returnUrl)
    {
        if (ModelState.IsValid)
        {
            AppUser user = await UserManager.FindAsync(details.Name, details.Password);

            if (user == null)
            {
                ModelState.AddModelError("", "Invalid name or password.");
            }
            else
            {
                ClaimsIdentity ident = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
                AuthManager.SignOut();
                AuthManager.SignIn(new AuthenticationProperties { IsPersistent = false }, ident);

                user.LastLoginDate = DateTime.UtcNow;
                await UserManager.UpdateAsync(user);

                PrincipalExSerializeModel serializeModel = new PrincipalExSerializeModel();
                serializeModel.UserExInfo = new UserExInfo()
                {
                    BusinessInfo = await
                        db.Businesses
                        .Where(b => user.Id.Equals(b.AspNetUserID))
                        .Select(b => new UserExBusinessInfo { BusinessID = b.BusinessID, Name = b.Name })
                        .ToListAsync()
                };

                JavaScriptSerializer serializer = new JavaScriptSerializer();

                string userData = serializer.Serialize(serializeModel);

                FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
                         1,
                         details.Name,
                         DateTime.Now,
                         DateTime.Now.AddMinutes(15),
                         false,
                         userData);

                string encTicket = FormsAuthentication.Encrypt(authTicket);
                HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket);
                Response.Cookies.Add(faCookie);

                return RedirectToLocal(returnUrl);
            }
        }
        return View(details);
    }

And finally in Global.asax.cs

    protected void Application_PostAuthenticateRequest(Object sender, EventArgs e)
    {
        HttpCookie authCookie = Request.Cookies[FormsAuthentication.FormsCookieName];

        if (authCookie != null)
        {
            FormsAuthenticationTicket authTicket = FormsAuthentication.Decrypt(authCookie.Value);
            JavaScriptSerializer serializer = new JavaScriptSerializer();
            PrincipalExSerializeModel serializeModel = serializer.Deserialize<PrincipalExSerializeModel>(authTicket.UserData);
            PrincipalEx newUser = new PrincipalEx(HttpContext.Current.User, serializeModel.UserExInfo);
            HttpContext.Current.User = newUser;
        }
    }

Now I can access the data in views and controllers simply by calling

User.ExInfo()

To log out I just call

AuthManager.SignOut();

where AuthManager is

HttpContext.GetOwinContext().Authentication
Vasily Ivanov
  • 351
  • 3
  • 5