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
- Click on New Project -> Select 'Web' from left pane -> Select 'ASP.NET Web Application' -> Enter name -> Click on 'OK'.
- 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:
- Enable Migration:
Enable-Migrations
- 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");
}
}
- 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
- To Add SignalR hub class, right click on the project -> add new item -> Select 'SignalR Hub Class' -> enter Name -> Click on 'Add'.
- Adding mapping of SignalR in startup.cs:
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
app.MapSignalR();
}
- 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; }
}
- 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.