Click here to Skip to main content
13,737,141 members
Click here to Skip to main content
Add your own
alternative version

Stats

4.3K views
15 bookmarked
Posted 16 May 2018
Licenced CPOL

Force Logout in Identity with SignalR

, 16 May 2018
Rate this:
Please Sign up or sign in to vote.
This article demonstrates how to implement force logout in Identity using SingalR.

Introduction

Lately, I was tasked with implementing Force Logout feature for one of the clients from Banking Industry. Security was paramount and Force Logout was one of the aspects they wanted us to integrate with the application. After a bit of research, I zeroed in on the method of adding SecurityStamp with the user claims and then compare the existing claim value with the database whenever user sends request to the server. Although this method served the purpose, there was one caveat: user won't be logged out real-time and will have to wait for the user to hit the request on the server.

Background

As the scenario was to log out user and redirect him to the login page real-time, I decided to accomplish task using SignalR. For the developers who are new to SignalR: it is a library provided by Microsoft for building real-time web application.

Walkthrough

I have created an MVC application to demonstrate how to perform force logout using SignalR, the source code can be obtained from GitHub.

Steps to Setup and Run SignalR in MVC Application

1. Creating an MVC Application

  1. Click on New Project -> Select 'Web' from left pane -> Select 'ASP.NET Web Application' -> Enter name -> Click on 'OK'.

  2. Select 'MVC' from Template, keep Authentication to 'Individual User Accounts' -> Click on 'OK'.

2. Migrations

Open Package Manager Console and execute the following commands to Create Database, Roles and Users:

  1. Enable Migration:
    Enable-Migrations
  2. Add Migration:
    Add-Migration CreateDB

    Once you have added migration, open Configuration.cs and replace seed method with the below code: it creates 2 Roles 'Admin', 'User' and 4 Users 'Admin', 'User1', 'User2', 'User3'.

    protected override void Seed(IdentityForceLogout.Models.ApplicationDbContext context)
         {
                if (!context.Roles.Any(r => r.Name == "Admin"))
                {
                    var store = new RoleStore<IdentityRole>(context);
                    var manager = new RoleManager<IdentityRole>(store);
                    var role = new IdentityRole { Name = "Admin" };
    
                    manager.Create(role);
                }
    
                if (!context.Roles.Any(r => r.Name == "User"))
                {
                    var store = new RoleStore<IdentityRole>(context);
                    var manager = new RoleManager<IdentityRole>(store);
                    var role = new IdentityRole { Name = "User" };
    
                    manager.Create(role);
                }
    
                if (!context.Users.Any(u => u.UserName == "Admin"))
                {
                    var store = new UserStore<ApplicationUser>(context);
                    var manager = new UserManager<ApplicationUser>(store);
                    var user = new ApplicationUser 
                       { UserName = "Admin", 
                         Email = "Admin@SomeDomain.com", EmailConfirmed = true };
    
                    manager.Create(user, "Admin123");
                    manager.AddToRole(user.Id, "Admin");
                }
    
                if (!context.Users.Any(u => u.UserName == "User1"))
                {
                    var store = new UserStore<ApplicationUser>(context);
                    var manager = new UserManager<ApplicationUser>(store);
                    var user = new ApplicationUser 
                      { UserName = "User1", 
                        Email = "User1@SomeDomain.com", EmailConfirmed = true };
    
                    manager.Create(user, "User123");
                    manager.AddToRole(user.Id, "User");
                }
    
                if (!context.Users.Any(u => u.UserName == "User2"))
                {
                    var store = new UserStore<ApplicationUser>(context);
                    var manager = new UserManager<ApplicationUser>(store);
                    var user = new ApplicationUser 
                      { UserName = "User2", 
                        Email = "User2@SomeDomain.com", EmailConfirmed = true };
    
                    manager.Create(user, "User123");
                    manager.AddToRole(user.Id, "User");
                }
    
                if (!context.Users.Any(u => u.UserName == "User3"))
                {
                    var store = new UserStore<ApplicationUser>(context);
                    var manager = new UserManager<ApplicationUser>(store);
                    var user = new ApplicationUser 
                        { UserName = "User3", 
                          Email = "User3@SomeDomain.com", EmailConfirmed = true };
    
                    manager.Create(user, "User123");
                    manager.AddToRole(user.Id, "User");
                }
    
            }
  3. Finally create database:
    Update-Database

3. Authorization

Decorate Index action inside Home Controller with [Authorize] attribute.

[Authorize]
public ActionResult Index()
{
    return View();
}

Create 2 more actions, Admin and Users in Home Controller and decorate them with Authorize Roles attribute.

[Authorize(Roles = "Admin")]
public ActionResult Admin()
  {            
       return View();
  }
[Authorize(Roles = "User")]
public ActionResult User()
  {
       return View();
  }

Implementing SignalR

Adding SignalR Hub Class

  1. To Add SignalR hub class, right click on the project -> add new item -> Select 'SignalR Hub Class' -> enter Name -> Click on 'Add'.

  2. Adding mapping of SignalR in startup.cs:
    public void Configuration(IAppBuilder app)
            {
                ConfigureAuth(app);
                app.MapSignalR();
            }
  3. Add user class:

    User class has 2 properties, Name to store username and Hashset of ConnectionIds to store IDs for each session created when user logs in.

    public class User
        {
            public string Name { get; set; }
            public HashSet<string> ConnectionIds { get; set; }
        }
  4. Decorate Hub class with Authorize attribute and replace the hello method with the below code inside Hub class:
    [Authorize]
        public class AuthHub : Hub
        {
            private static readonly ConcurrentDictionary<string, User> ActiveUsers  = 
              new ConcurrentDictionary<string, User>(StringComparer.InvariantCultureIgnoreCase);
            public IEnumerable<string> GetConnectedUsers()
            {
                return ActiveUsers.Where(x => {
    
                    lock (x.Value.ConnectionIds)
                    {
                        return !x.Value.ConnectionIds.Contains
                                (Context.ConnectionId, StringComparer.InvariantCultureIgnoreCase);
                    }
    
                }).Select(x => x.Key);
            }
    
            public override Task OnConnected()
            {
                string userName = Context.User.Identity.Name;
                string connectionId = Context.ConnectionId;
    
                var user = ActiveUsers.GetOrAdd(userName, _ => new User
                {
                    Name = userName,
                    ConnectionIds = new HashSet<string>()
                });
    
                lock (user.ConnectionIds)
                {
    
                    user.ConnectionIds.Add(connectionId);
                    
                }
    
                return base.OnConnected();
            }
    
            public override Task OnDisconnected(bool stopCalled)
            {
                string userName = Context.User.Identity.Name;
                string connectionId = Context.ConnectionId;
    
                User user;
                ActiveUsers.TryGetValue(userName, out user);
    
                if (user != null)
                {
                    lock (user.ConnectionIds)
                    {
                        user.ConnectionIds.RemoveWhere(cid => cid.Equals(connectionId));
    
                        if (!user.ConnectionIds.Any())
                        {
    
                            User removedUser;
                            ActiveUsers.TryRemove(userName, out removedUser);
                        }
                    }
                }
    
                return base.OnDisconnected(stopCalled);
            }
    
            private User GetUser(string username)
            {
                User user;
                ActiveUsers.TryGetValue(username, out user);
    
                return user;
            }
    
            public void forceLogOut(string to)
            {
                User receiver;
                if (ActiveUsers.TryGetValue(to, out receiver))
                {
                    IEnumerable<string> allReceivers;
                    lock (receiver.ConnectionIds)
                    {
                        allReceivers = receiver.ConnectionIds.Concat(receiver.ConnectionIds);      
                    }
    
                    foreach (var cid in allReceivers)
                    {
                        Clients.Client(cid).Signout();
                    }
                }
            }
        }

Following changes are done in Hub class:

  • Added property to the Hub class 'ActiveUsers' of type 'ConcurrentDictionary' with Key as 'String' and values as 'User' class we defined above.
  • Added function 'GetConnectedUsers' which returned string of all users except the current one.
  • Overrided 'OnConnected' function of the Hub class, add 'Username' and 'connectionid' to the 'AcitveUsers'.
  • Overrided 'OnDisconnected' function of the Hub class, remove the user from the 'AcitveUsers'.
  • Finally, added 'forceLogOut' method which invokes 'Signout' - Client side JavaScript function of the specific client.

Client-Side Implementation

Provide references to the Jquery file, SignalR core JavaScript file and SignalR generated proxy JavaScript file.

<script src="~/Scripts/jquery.signalR-2.1.2.js"></script>
<script src="/signalr/hubs"></script>

To establish connection to SignalR hub, first create connection object:

var auth = $.connection.authHub; 

Define 'PopulateActiveUsers' function inside done function of Hubs connection start call to prevent JavaScript error due to no connectivity.

$.connection.hub.start().done(function () {

               function PopulateActiveUsers()
               {
                   $("#usersTable").empty();
                   auth.server.getConnectedUsers().done(function (users) {
                       $.each(users, function (i, username) {
                           $("#usersTable").append("<tr><th scope='row'>" +
                           (i + 1) + "</th><td>" + username +
                           "</td><td><a href='javascript:void(0)' data-user='" +
                           username + "' class='btn btn-primary btn-sm btn-logout'>
                           Logout User</a></td></tr>");
                       });
                   });
               }

               $("#displayActiveUsers").on("click", function () {
                   PopulateActiveUsers();
               });

               $('body').on('click', 'a.btn-logout', function () {
                   var username = $(this).attr('data-user');
                   auth.server.forceLogOut(username);
               });

           });

Define client-side function 'Signout' which logs off user and removes the user from ActiveUsers dictionary.

auth.client.Signout = function () {
                $('#logoutForm').submit();
                $.connection.hub.stop();
            };

Process

When user logs in, Onconnected method is invoked on the server and user is added to the ActiveUsers dictionary.

When admin logs in and clicks on the show active users, SignalR invokes 'getConnectedUsers' on the server and populate users in table based on the result set returned.

When Admin clicks on the Logout button, forceLogOut method is invoked on the server with the username as a parameter which logs out specific user.

The full source code for this article is available on GitHub at https://github.com/seenanK/IdentityForceLogout.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Seenan Khalife
Technical Lead
United Arab Emirates United Arab Emirates
Full Stack Developer, Technology Enthusiast and Passionate programmer with zeal to learn new technology and excel in existing. Expertise in enterprise applications across multiple domains and industries.

You may also be interested in...

Comments and Discussions

 
-- There are no messages in this forum --
Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web05-2016 | 2.8.180920.1 | Last Updated 16 May 2018
Article Copyright 2018 by Seenan Khalife
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid