11

I'm using ASP.NET Core MVC with CookieAuthentication. Is there a way I can sign all users out at once? I tried resetting IIS - didn't work. I tried deleting all the users' sessions (I'm using a database for session storage) - didn't work.

Any ideas?

3
  • 1
    To logout all users, the encryption key may be changed or authentication cookie may be renamed. Commented Jul 3, 2018 at 18:23
  • @Zygimantas check this answer. stackoverflow.com/a/36050939/5233410 Commented Jul 3, 2018 at 18:41
  • @Nkosi: Identity server is used in your link. The question does not mention it, just plain simple cookie authentication. Commented Jul 3, 2018 at 19:00

4 Answers 4

6

You can use CookieAuthenticationOptions.SessionStore Property, to store identity information in server side so you can clear it all when you need.

public void ConfigureServices(IServiceCollection services)
{
    MemoryCacheTicketStore memoryCacheTicketStore = new MemoryCacheTicketStore();
    services.AddSingleton<MemoryCacheTicketStore>(memoryCacheTicketStore);

    services.AddAuthentication().AddCookie(cfg =>
    {
        cfg.SessionStore = memoryCacheTicketStore;
    });
}

public class SessionController : Controller
{
    private readonly MemoryCacheTicketStore memoryCacheTicketStore;

    public SessionController(MemoryCacheTicketStore memoryCacheTicketStore)
    {
        this.memoryCacheTicketStore = memoryCacheTicketStore;
    }

    public Task ClearAllSession()
    {
        return memoryCacheTicketStore.ClearAll();
    }
}

public class MemoryCacheTicketStore : ITicketStore
{
    private const string KeyPrefix = "AuthSessionStore-";
    private IMemoryCache _cache;

    public MemoryCacheTicketStore()
    {
        _cache = new MemoryCache(new MemoryCacheOptions());
    }

    public async Task ClearAll()
    {
        _cache.Dispose();
        _cache = new MemoryCache(new MemoryCacheOptions());
    }

    public async Task<string> StoreAsync(AuthenticationTicket ticket)
    {
        var guid = Guid.NewGuid();
        var key = KeyPrefix + guid.ToString();
        await RenewAsync(key, ticket);
        return key;
    }

    public Task RenewAsync(string key, AuthenticationTicket ticket)
    {
        var options = new MemoryCacheEntryOptions();
        var expiresUtc = ticket.Properties.ExpiresUtc;
        if (expiresUtc.HasValue)
        {
            options.SetAbsoluteExpiration(expiresUtc.Value);
        }
        options.SetSlidingExpiration(TimeSpan.FromHours(1)); // TODO: configurable.

        _cache.Set(key, ticket, options);

        return Task.FromResult(0);
    }

    public Task<AuthenticationTicket> RetrieveAsync(string key)
    {
        AuthenticationTicket ticket;
        _cache.TryGetValue(key, out ticket);
        return Task.FromResult(ticket);
    }

    public Task RemoveAsync(string key)
    {
        _cache.Remove(key);
        return Task.FromResult(0);
    }
}
Sign up to request clarification or add additional context in comments.

Comments

4

With CookieAuthentication, the cookie is simply an encrypted string containing the user's name, roles, and auxilliary data. In short, it identifies the user, not the session. Killing sessions does not invalidate the cookie.

That being said, you can stuff a session identifier or other token in the cookie's auxiliary data, and then validate that during the authentication process. An example of someone trying to this can be found here.

Another option is instead of invalidating sessions you can temporarily disable users in your user repository. Here is an example using ASPNET Identity 2.0.

A third (nuclear) option is to change the machine key on all web servers, which will render any old forms authentication cookies unreadable, forcing all users to sign on again.

Comments

1

This is a case where the app needs to react to back-end user access changes. The authentication cookie will secure the application, but, remains valid for the lifetime of the cookie. With a valid cookie, the end-user will not see any changes until they log out or the cookie expires.

The most suitable solution in ASP.NET Core, to validate authentication cookie changes is through Cookie Authentication Events. The validation event will perform back-end lookups from identity claims in the cookie. This event extends CookieAuthenticationEvents. Here we override the ValidatePrincipal method.

using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Claims;

public class RevokeAuthenticationEvents : CookieAuthenticationEvents
{
    private readonly IMemoryCache _cache;
    private readonly ILogger _logger;

    public RevokeAuthenticationEvents(IMemoryCache cache, ILogger<RevokeAuthenticationEvents> logger)
    {
        _cache = cache;
        _logger = logger;
    }

    public override Task ValidatePrincipal(CookieValidatePrincipalContext context)
    {
        var userId = context.Principal.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;

        if (_cache.Get<bool>("revoke-" + userId))
        {
            context.RejectPrincipal();

            _cache.Remove("revoke-" + userId);
            _logger.LogDebug("{Message}", "Access has been revoked for: " + userId + ".");
        }

        return Task.CompletedTask;
    }
}

To have IMemoryCache set by DI, put AddMemoryCache inside the ConfigureSerices method in the Startup class. Calling context.RejectPrincipal() has an immediate effect and kicks users back out to Login to get a new authentication cookie.

Register the event inside the ConfigureServices method in the Startup class.

options.EventsType = typeof(RevokeAuthenticationEvents);
services.AddScoped<RevokeAuthenticationEvents>();

Now you can logout users by setting the cache inside the corresponding Method of your Controller:

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using System.Security.Claims;

public class AuthenticationController : Controller
{
    private readonly IUsersService _usersService;
    private readonly IMemoryCache _cache;

    public AuthenticationController(IUsersService usersService, IMemoryCache cache)
    {
        _usersService = usersService;
        _cache = cache;
    }

    [HttpGet]
    public IActionResult Logout()
    {
        // Logout currently logged in user
        var principal = HttpContext.User as ClaimsPrincipal;
        var userId = principal?.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value;

        _cache.Set("revoke-" + userId, true);

        return View();
    }

    [HttpGet]
    public async Task LogoutAll()
    {
        // Fetch users from database
        var users = await _usersService.GetAllUsersAsync();

        // Logout all users
        foreach (var user in users)
        {
            var userId = user.Id;
            _cache.Set("revoke-" + userId, true);
        }
    }
}

Note that this relies on in-memory persistence which gets set in the controller action methods. Keep in mind that this type of event runs once per every request, so you want to use an efficient caching strategy.

Login

Make sure you add the claim inside the action method responsible for login in your Controller.

var claims = new List<Claim>
{
    // Set the user id
    new Claim(ClaimTypes.NameIdentifier, userId)
};

var claimsIdentity = new ClaimsIdentity(claims,
    CookieAuthenticationDefaults.AuthenticationScheme);
var authProperties = new AuthenticationProperties();

await HttpContext.SignInAsync(
    CookieAuthenticationDefaults.AuthenticationScheme,
    new ClaimsPrincipal(claimsIdentity),
    authProperties);

For more information see Use cookie authentication without ASP.NET Core Identity.

Comments

-1

it's very simple. change the login cookie name By changing the name, all cookies are invalid, forcing you to log in again! either via API, or MVC Identity

in startup.cs, change the default name to anything.

 options.Cookie.Name = "NewName";

Complete Example:

  services.ConfigureApplicationCookie(options =>
            {
                options.Cookie.Name = "NewName"; //<-- Here
                options.Cookie.HttpOnly = true;
              ...
                options.Events = options.Events ?? new CookieAuthenticationEvents();
                var onForbidden = options.Events.OnRedirectToAccessDenied;
                var onUnauthorized = options.Events.OnRedirectToLogin;
                options.Events.OnRedirectToAccessDenied = (context) => OnRedirect(context, onForbidden, HttpStatusCode.Forbidden);
                options.Events.OnRedirectToLogin = (context) => OnRedirect(context, onUnauthorized, HttpStatusCode.Unauthorized);
            });

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.