3

I am building an MVC 5 application and have come to the following problem: I want to show a menu item to the user, after the user has logged in, if the user has an Agreement with me.

I want to set a session variable at the moment the user logs in like:

Session["HasAgreement"] = Agreement.HasAgreement(userId);

and then in my _Layout.cshtml file where I build my menu do something like:

@if (Session["HasAgreement"] == "True")
{
   <li>@Html.ActionLink("Agreement", "Agreement", "Home")</li>
}

My problem arises in the AccountController where I have added the logic to the standard Login Action:

public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
{
    if (!ModelState.IsValid)
    {
        return View(model);
    }

    var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);
    switch (result)
    {
        case SignInStatus.Success:
            var userId = User.Identity.GetUserId();
            Session["HasAgreement"] = Agreement.HasAgreement(userId);
            return RedirectToLocal(returnUrl);
        case SignInStatus.LockedOut:
            return View("Lockout");
        case SignInStatus.RequiresVerification:
            return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });
        case SignInStatus.Failure:
        default:
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
    }
}

This is the standard MVC 5 login - except that I have added the two lines right after the "case SignInStatus.Success:" where I try to get the userId and then set the Session variable.

My problem is that at this point in thime the User is not authenticated(I thought that happened in the SignInManager above).

How do I set the session variable right after the user logs in?

st4hoo
  • 2,196
  • 17
  • 25
olf
  • 860
  • 11
  • 23
  • possible duplicate of [Asp.net Identity : User.Identity.GetUserId() is always null and User.Identity.IsAuthenticated is alway false](http://stackoverflow.com/questions/25439275/asp-net-identity-user-identity-getuserid-is-always-null-and-user-identity-is) – st4hoo Mar 06 '15 at 13:00
  • Yes, that is true! It is a duplicate. I just couldn't find the answer when I searched before! Thank you. – olf Mar 06 '15 at 13:11
  • st4hoo, if you leave an answer I can mark it as correct. – olf Mar 07 '15 at 19:41

7 Answers7

2

The new session isn't set until you hit the next action. This is a common issue with trying to use the result for anything other than redirection. The best course of action would be to use the result to redirect to another action, in which you will then be able to access the session.

Assuming when your user logs in they go to a "dashboard", it might look something like:

SignInStatus.Success case:

case SignInStatus.Success:
    return RedirectToAction("Dashboard");

If you require the ability to return to numerous actions, you can return the action name instead of a url and simply do RedirectToAction(returnAction). Obviously if you need to specify a controller as well, you'll need to post a returnController too.

Dashboard action:

[Authorize]
public ActionResult Dashboard() {
    var userId = User.Identity.GetUserId();

    Session["HasAgreement"] = Agreement.HasAgreement(userId);

    return View();
}
Inspector Squirrel
  • 2,548
  • 2
  • 27
  • 38
  • I don't think your first sentence is true... `RedirectToLocal` (used in the newest Identity as well as what the OP referenced) is it's own `ActionResult` in the `AccountController`. Therefore, by your definition and example, he should be able to put `Session["HasAgreement"] = Agreement.HasAgreement(userId);` in the `RedirectToLocal` and have it work? I'm dealing with something nearly identical and it doesn't work for me. – Sum None Oct 30 '15 at 19:49
1

Your problem is not when you update the session variable, but what version of the session your layout page has got.

session isn't the best option for passing data between views. try ViewBag

(although you should always try to use a ViewModel where possible!) (and you can use the session AS WELL, for the next page load)

Ewan
  • 1,261
  • 1
  • 14
  • 25
  • ahh but its in the layout – Ewan Mar 06 '15 at 13:13
  • Sippy, I agree that in most cases I would use a view model. In this special case I will use the session variable though as it is something that is used (for the logged in user) all over the app (the _Layout.cshtml is used by all my views) - and I would not like to populate my view model for each and every view/page. – olf Mar 06 '15 at 13:13
  • I've had a similar problem, the session variable is weird and will not be updated with your value when the layout tries to read it – Ewan Mar 06 '15 at 13:14
1

I'm not sure where your Agreement object is coming from but you have access to the User property in the View so you could potentially do something like this:

_Layout.cshtml

@if (Agreement.HasAgreement(User.Identity.GetUserId()))
{
   <li>@Html.ActionLink("Agreement", "Agreement", "Home")</li>
}

this also assumes that HasAgreement returns a bool which if it doesn't, it really should.

dav_i
  • 27,509
  • 17
  • 104
  • 136
  • Yes the Agreement.HasAgreement returns a bool. And instead of re-calling the method I only want to call it once (as there are some heavy db-calls involved which I do want to avoid doing more than once). So I store this single variable in a session variable "once and for all" instead. – olf Mar 07 '15 at 19:40
  • @olf then you should cache within the method. Keep a dictionary of ids with bool and populate that once, read many. – dav_i Mar 07 '15 at 19:43
1

This might not work in all cases, but I found the easiest way to set a session variable at Login was to move the logic to the controller action returned by RedirectToLocal(). In my case I made a new post-login entry point to the application called "Main", that has it's own view/controller. On login the user is always redirected here first, so it guarantees my Session data gets set.

First I changed RedirectToLocal() in the AccountController:

private ActionResult RedirectToLocal(string returnUrl)
{
    if (Url.IsLocalUrl(returnUrl))
    {
        return Redirect(returnUrl);
    }
    //This is the template default   
    //return RedirectToAction("Index", "Home");         

    //This is my new entry point. 
    return    RedirectToAction("Index", "Main");     }

Inside my MainController I can access the User object and set my data:

// GET: Main
public ActionResult Index()
{
    ApplicationUser sessionuser = db.Users.Find(User.Identity.GetUserId());
    Session.Add("UserName", sessionuser.UserName);
    return View();
}

And for good measure I'm wiping the session data on logoff:

public ActionResult LogOff()
{
    Session.RemoveAll(); //Clear all session variables
    //...              
}
Andrew Lockhart
  • 164
  • 2
  • 3
0

As an option:

Add new partial view - call it Agreement.

@model bool
@if (Model)
{
   <li>@Html.ActionLink("Agreement", "Agreement", "Home")</li>
}

Add new action to your, say, Account controller, call it Agreement

public PartialViewResult Agreement(){
    var userId = User.Identity.GetUserId();
    bool hasAgreement = Agreement.HasAgreement(userId); // This will be your model
    return PartialView("Agreement", hasAgreement);
}

And in your layout do:

@Html.RenderAction("Agreement", "Account")
Dmytro
  • 16,668
  • 27
  • 80
  • 130
  • Another good solution, but as commented above I really want to store it in a session variable right at login in order not to continiously call the Agreement.HasAgreement method. – olf Mar 07 '15 at 19:41
0

I have the exact same issue and appear to be using the same version of Identity as you (the OP). I tried what you did (prior to finding this) as well as followed Sippy's advice and put it in RedirectToLocal ActionResult in the AccountController (which didn't work):

In the meantime, I put it in the Global.Asax.cs under:

public void Profile_OnMigrateAnonymous(object sender, ProfileMigrateEventArgs args)
{
    ...//other code (or not) on migrate anonymous user to authenticated
    Session["HasAgreement"] = Agreement.HasAgreement(userId);
}

I also created this void in my Global.asax.cs (catches expired sessions but still logged in):

void Session_Start(object sender, EventArgs e)
{
     Session["HasAgreement"] = Agreement.HasAgreement(userId);
}

It works and also updates if the session expires. However, one of the downsides of this is that the session variable will not be updated when a user Logs Off (via Identity). However, I also tried putting my client extension in the LogOff ActionResult after AuthenticationManager.SignOut(); and it doesn't work.

I need my session variable to update after Log Off too, so this won't be my final solution, but it may be good enough for you?

I'll come back and update this if I find a better way, but right now, it's good enough and going on my TODO list.

UPDATE:

To catch the user's logoff event, I used Sippy's idea and changed my LogOff ActionResult in the AccountController to this:

        //
        // POST: /Account/LogOff
        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult LogOff()
        {
            AuthenticationManager.SignOut();
            return RedirectToAction("SetSessionVariables", "Account");
        }

        [AllowAnonymous]
        public ActionResult SetSessionVariables()
        {
            Session["HasAgreement"] = Agreement.HasAgreement(userId);
            return RedirectToAction("Index", "Home");
        }

I think this encompasses all logon/logoff/session scenarios. I might see if I can incorporate what I did for the LogOff into the Logon successful redirect (more so to see when the user is actually "authenticated" than anything).

Sum None
  • 2,164
  • 3
  • 27
  • 32
0

You can use user Claims to extend Identity data.

Implementation for instance here: How to extend available properties of User.Identity

Petr
  • 1,193
  • 1
  • 15
  • 27