3

I use the following code to update the claims of a user in my web application. However, to update the claims/cookie of this user, I want to force him to login again. So essentially I want to expire his cookie after I update the claims. Any idea how to do this?

await _signInManager.RefreshSignInAsync(user); is the first thing I tried, but fails because I'm updating the claims of another user (the one that is currently logged in) :)

All other examples I found are more or less the same as RefreshSignInAsync and do not deal with the fact that I'm updating the claims of another user.

 public async Task<IActionResult> AddClaimPost(string id)
 {
     var user = _context.ApplicationUser
         .SingleOrDefault(m => m.Id == id);


     foreach(var item in Request.Form)
     {
         if (item.Key.Contains("Claim"))
         {
             if (item.Value.Contains("true"))
             {
                 if (!User.HasClaim(item.Key, item.Key))
                 {
                     var result = await _userManager.AddClaimAsync(user, new Claim(item.Key, item.Key));
                 }
             }
             else
             {
                 var result2 = await _userManager.RemoveClaimAsync(user, new Claim(item.Key, item.Key));
             }
         }
     }

     await _signInManager.RefreshSignInAsync(user);

     return RedirectToAction("Overview");
 }
Steven Jeuris
  • 18,274
  • 9
  • 70
  • 161
MartinH
  • 1,440
  • 13
  • 24
  • 1
    Interestingly, [you seem to have made the same mistake with calling `RefreshSignInAsync` as I did](https://stackoverflow.com/q/51571852/590790). Maybe [the documentation of that method](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.signinmanager-1.refreshsigninasync?view=aspnetcore-2.1) is not all that clear? :) – Steven Jeuris Jul 30 '18 at 12:01
  • 1
    Rather than asking the same question as you did, I updated yours to reflect what I believe you were asking (based on my understanding). Could you verify whether I did not misrepresent what you originally asked for? – Steven Jeuris Jul 30 '18 at 12:53
  • Yeah, your changes are fine. I was somewhat in a hurry when creating the question. The docs are indeed somewhat unclear. Maybe create an issue on github, which is possible quite recently: https://github.com/aspnet/ApiDocs/blob/live/api/xml/Microsoft.AspNetCore.Identity/SignInManager%601.xml ? – MartinH Jul 30 '18 at 19:01
  • 1
    Ah, I see you already filed a github issue! Never mind my previous comment :) – MartinH Jul 30 '18 at 19:10

2 Answers2

0

After searching a few days I discovered that what I want is not possible. You cannot force logging the user out without putting the cookie timespan to 0

options.Cookies.ApplicationCookie.ExpireTimeSpan = 0;

In this case it will check the cookie every time the user makes a request. With the following code you can than force the user to login again:

await _userManager.UpdateSecurityStampAsync(user);

MartinH
  • 1,440
  • 13
  • 24
  • 1
    Wouldn't it be an option to (ideally conditionally) call `RefreshSignInAsync` when the user whose sign in you want to update performs a request? – Steven Jeuris Jul 30 '18 at 12:03
  • That was also an option I considered. But this will need an additional flag to keep track of when to call `RefreshSignInAsync`. The solution I used was fire once and forget about it. Refreshing the cookie often did not seem to be a problem for me... – MartinH Jul 30 '18 at 19:05
  • 1
    There's a performance cost as each request will require hitting the database to check the security stamp. Not an issue for dozens of interactive users, but it's a problem if you have hundreds or thousands of users. – Mark G Oct 15 '19 at 20:07
  • This breaks the "remember me" box. – Neo May 29 '22 at 20:34
0

I don't recommend the 0 expire timespan approach.

If you have a redis server (or any other persistent data store that is performant) you can do something like:

await redis.StringSetAsync("refresh_login_" + user.Id, "1", null);

Then on every page load you will check this redis value and refresh the signin if the key is set for you:

Filters/BaseActionFilter.cs:

public class BaseActionFilter: IAsyncActionFilter, IAsyncPageFilter
{
    public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    { // For classic controllers

        await PerformPageTasks(context.HttpContext);
        await next();
    }
    
    
    public async Task OnPageHandlerExecutionAsync(PageHandlerExecutingContext context,
        PageHandlerExecutionDelegate next)
    { // For razor pages in Areas/
        if ((context.HandlerInstance is PageModel page))
        {
            await PerformPageTasks(context.HttpContext);
        }

        await next.Invoke();
    }

    public async Task PerformPageTasks(HttpContext context)
    {
        var signinManager = context.RequestServices.GetService<SignInManager<MyWebUser>>();
        if (signinManager.IsSignedIn(context.User))
        {
            var cache = context.RequestServices.GetService<IDistributedCache>();
            var redis = (await ((RedisCache)cache).GetConnectionAsync()).GetDatabase();

            var userManager = context.RequestServices.GetService<UserManager<MyWebUser>>();
            var user = await userManager.GetUserAsync(context.User);

            if ((await redis.StringGetAsync("refresh_login_" + user.Id)) == "1")
            {
                await redis.KeyDeleteAsync("refresh_login_" + user.Id);
                // refresh the user
                await signinManager.RefreshSignInAsync(user);
            }        
        }
    }

    public async Task OnPageHandlerSelectionAsync(PageHandlerSelectedContext context)
    {
        await Task.CompletedTask;
    }
}

Startup.cs:

services.AddMvc(o =>
{
    ...
    o.Filters.Add(new BaseActionFilter());
}).AddHybridModelBinder();

If you only use traditional controllers or Areas/ razor pages then you can adapt the code accordingly.

Note this requires the user to make an additional page load before the claims are set, so for things like [Authorize] you would need to put this code earlier in the chain and I'm not sure exactly how to do that.

Neo
  • 474
  • 6
  • 13