Click here to Skip to main content
15,881,803 members
Articles / Programming Languages / Javascript

Force Logout in Identity with SignalR

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
16 May 2018CPOL3 min read 20.6K   16   3
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'.

    Image 1

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

    Image 2

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'.

    C#
    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.

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

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

C#
[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'.

    Image 3

  2. Adding mapping of SignalR in startup.cs:
    C#
    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.

    C#
    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:
    C#
    [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.

HTML
<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:

JavaScript
var auth = $.connection.authHub; 

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

JavaScript
$.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.

JavaScript
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.

This article was originally posted 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)


Written By
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.

Comments and Discussions

 
QuestionImplementing connections on database Pin
Member 1535691113-Sep-21 11:42
Member 1535691113-Sep-21 11:42 
AnswerRe: Implementing connections on database Pin
Seenan Khalife15-Sep-21 2:07
professionalSeenan Khalife15-Sep-21 2:07 
QuestionOnConnected Method doesn't fire Pin
Ahmed Shaker018-Aug-19 20:54
Ahmed Shaker018-Aug-19 20:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.