Search code examples
javascriptasp.net-mvcsignalrasp.net-identitylogout

How to make any view aware that the User Roles (ASP.NET MVC Identity) have changed in order to force any user to logout while he or she is browsing?


I think the question title is self-explanatory, maybe just a precision when I say while "he or she is browsing" I am thinking in terms of propagation or signal.

I don't want that he or she has to browse another place just to figure out that the Identity SecurityStamp has changed and has been signed out to be then redirected to the home page, I am already doing this but I am wondering if there a framework (I suspect most likely JS) that would make the operation a bit more "real-time".

[EDIT]
Probably a job for SignalR, I haven't tried this out, yet.


Solution

  • I managed to get a working solution with SignalR

    First, pay attention to the order in which SignalR is setup in the Startup.Auth.cs How to send message via SignalR to a specific User(Identity Id)? and also create an implementation of the IUserIdProvider that is going to be registred only AFTER the Cookies and OwinContext in order to make it able to leverage the Identity User field (i.e. non-null).

    public partial class Startup
    {
        public void ConfigureAuth(IAppBuilder appBuilder)
        {
            // Order matters here...
            // Otherwise SignalR won't get Identity User information passed to Id Provider...
            ConfigOwinContext(appBuilder);
            ConfigCookies(appBuilder);
            ConfigSignalR(appBuilder);
        }
    
        private static void ConfigOwinContext(IAppBuilder appBuilder)
        {
            appBuilder.CreatePerOwinContext(ApplicationDbContext.Create);
            appBuilder.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            appBuilder.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
            appBuilder.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);
            appBuilder.CreatePerOwinContext(LdapAdEmailAuthenticator.Create);
        }
    
        private static void ConfigCookies(IAppBuilder appBuilder)
        {
            appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>
                    (
                        TimeSpan.FromHours(4),
                        (manager, user) => user.GenerateUserIdentityAsync(manager)
                    )
                }
            });
            appBuilder.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
            appBuilder.UseTwoFactorSignInCookie(DefaultAuthenticationTypes.TwoFactorCookie, TimeSpan.FromMinutes(5));
            appBuilder.UseTwoFactorRememberBrowserCookie(DefaultAuthenticationTypes.TwoFactorRememberBrowserCookie);
        }
    
        private static void ConfigSignalR(IAppBuilder appBuilder)
        {
            appBuilder.MapSignalR();
            var idProvider = new HubIdentityUserIdProvider();
            GlobalHost.DependencyResolver.Register(typeof(IUserIdProvider), () => idProvider);
        }
    }
    
    public class HubIdentityUserIdProvider : IUserIdProvider
    {
        public string GetUserId(IRequest request)
        {
            return request == null
                ? throw new ArgumentNullException(nameof(request))
                : request.User?.Identity?.GetUserId();
        }
    }
    

    Second, declare a hub on the server-side

    public class UserHub : Hub
    {
    }
    

    Third, in a controller (API or not) where a change that involves a logout of a specific user, force a signout + an update of the identity securitystamp:

     var userHub = GlobalHost.ConnectionManager.GetHubContext<UserHub>();
     userHub.Clients.User(userId).send("Roles added: " + rolesToAdd.Join() + Environment.NewLine + "Roles removed: " + rolesToRemove.Join());
    
     return Request.CreateResponse(HttpStatusCode.OK);
    

    Fourth, use the hub on the JS client-side, I created a partial view which is only used when the current user is authenticated, LoggedOutPartialView.cshtml:

    @if (Request.IsAuthenticated)
    {
        <div class="modal fade" id="loggedOutModal" tabindex="-1" role="dialog" aria-labelledby="loggedOutModalLabel">
            <div class="modal-dialog" role="document">
                <div class="modal-content">
                    <div class="modal-header">
                        <h4 class="modal-title" id="loggedOutModalLabel">Notification</h4>
                    </div>
                    <div class="modal-body">
                        <h6 class="align-center">Sorry, but it seems that you just have been logged out!!!</h6>
                    </div>
                    <div class="modal-footer">
                        <button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
                    </div>
                </div>
            </div>
        </div>
    
        <script>
    
            $(function() {
                var userHub = $.connection.userHub;
    
                console.log(userHub.client);
    
                userHub.client.logout= function (message) {
                    $('#loggedOutModal').modal('show');
                };
    
                $.connection.hub.start().done(function () {
                });
            });
    
        </script>
    }