When the same user ID is trying to log in on multiple devices, how do I kill the session on the other device?

62,285

Solution 1

I came up with a pretty awesome solution to this. What I've implemented was when user "Bob" logs in from their PC, and then the same user "Bob" logs in from another location, the log-in from the first location (their PC) will be killed while allowing the second log-in to live. Once a user logs in, it inserts a record into a custom table I created called "Logins". Upon a successful log-in, one record will be inserted into this table with values for "UserId, SessionId, and LoggedIn". UserId is pretty self-explanatory, SessionId is the current Session ID (explained below how to get), and LoggedIn is simply a Boolean that's initially set to True upon a successful user log-in. I place this "insert" logic inside my Login method of my AccountController upon successful validation of the user- see below:

Logins login = new Logins();
login.UserId = model.UserName;
login.SessionId = System.Web.HttpContext.Current.Session.SessionID;;
login.LoggedIn = true;

LoginsRepository repo = new LoginsRepository();
repo.InsertOrUpdate(login);
repo.Save();

For my situation, I want to place the check on each of my controllers to see if the currently logged in user is logged in elsewhere, and if so, kill the other session(s). Then, when the killed session tries to navigate anywhere I placed these checks on, it'll log them out and redirect them to the Log-in screen.

I have three main methods that does these checks:

IsYourLoginStillTrue(UserId, SessionId);
IsUserLoggedOnElsewhere(UserId, SessionId);
LogEveryoneElseOut(UserId, SessionId);

Save Session ID to Session["..."]

Before all of this though, I save the SessionID to the Session collection inside the AccountController, inside the Login ([HttpPost]) method:

if (Membership.ValidateUser(model.UserName, model.Password))
{
     Session["sessionid"] = System.Web.HttpContext.Current.Session.SessionID;
...

Controller Code

I then place logic inside my controllers to control the flow of the execution of these three methods. Notice below that if for some reason Session["sessionid"] is null, it'll just simply assign it a value of "empty". This is just in case for some reason it comes back as null:

public ActionResult Index()
{
    if (Session["sessionid"] == null)
        Session["sessionid"] = "empty";

    // check to see if your ID in the Logins table has LoggedIn = true - if so, continue, otherwise, redirect to Login page.
    if (OperationContext.IsYourLoginStillTrue(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString()))
    {
        // check to see if your user ID is being used elsewhere under a different session ID
        if (!OperationContext.IsUserLoggedOnElsewhere(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString()))
        {
            return View();
        }
        else
        {
            // if it is being used elsewhere, update all their Logins records to LoggedIn = false, except for your session ID
            OperationContext.LogEveryoneElseOut(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString());
            return View();
        }
    }
    else
    {
        FormsAuthentication.SignOut();
        return RedirectToAction("Login", "Account");
    }
}

The Three Methods

These are the methods I use to check to see if YOU are still logged in (i.e. make sure you weren't kicked off by another log-in attempt), and if so, check to see if your User ID is logged in somewhere else, and if so, kick them off by simply setting their LoggedIn status to false in the Logins table.

public static bool IsYourLoginStillTrue(string userId, string sid)
{
    CapWorxQuikCapContext context = new CapWorxQuikCapContext();

    IEnumerable<Logins> logins = (from i in context.Logins
                                  where i.LoggedIn == true && i.UserId == userId && i.SessionId == sid
                                  select i).AsEnumerable();
    return logins.Any();
}

public static bool IsUserLoggedOnElsewhere(string userId, string sid)
{
    CapWorxQuikCapContext context = new CapWorxQuikCapContext();

    IEnumerable<Logins> logins = (from i in context.Logins
                                  where i.LoggedIn == true && i.UserId == userId && i.SessionId != sid
                                  select i).AsEnumerable();
    return logins.Any();
}

public static void LogEveryoneElseOut(string userId, string sid)
{
    CapWorxQuikCapContext context = new CapWorxQuikCapContext();

    IEnumerable<Logins> logins = (from i in context.Logins 
                                  where i.LoggedIn == true && i.UserId == userId && i.SessionId != sid // need to filter by user ID
                                  select i).AsEnumerable();

    foreach (Logins item in logins)
    {
        item.LoggedIn = false;
    }

    context.SaveChanges();
}

EDIT I just also want to add that this code ignores the capability of the "Remember Me" feature. My requirement didn't involve this feature (in fact, my customer didn't want to use it, for security reasons) so I just left it out. With some additional coding though, I'm pretty certain that this could be taken into consideration.

Solution 2

You will have to store the information that someone has logged in into the database. This would allow you to verify if the user already has an existing session. Out of the box the forms authentication module in ASP.NET works with cookies and there's no way for you to know on the server whether the user has cookies on other devices unless of course you store this information on the server.

Solution 3

What you probably want to do is when a user logs in, you save their session id in the database somewhere. Then on every page you access, you have to check if the current session id is the same as what's stored in the database, and if not you sign them out.

You will probably want to create a base controller that does this in the OnAuthorization or OnActionExecuting methods. Another option would be to create your own Authorization filter (i'd prefer that myself, actually, as I don't like common base classes like that).

In that method you would access the database and check the session id.

Be aware that his is not foolproof. It's possible for someone to copy the session cookie and get around this, though that's obscure enough that most people probably wouldn't know how to do that, and annoying enough that those that do wouldn't bother.

You could also use IP address, but that's the same deal. Two people behind a proxy or nat firewall would appear to be the same user.

Solution 4

Here's a method that is slightly simpler than the accepted answer.

public static class SessionManager
    {
        private static List<User> _sessions = new List<User>();

        public static void RegisterLogin(User user)
        {
            if (user != null)
            {
                _sessions.RemoveAll(u => u.UserName == user.UserName);
                _sessions.Add(user);
            }
        }

        public static void DeregisterLogin(User user)
        {
            if (user != null)
                _sessions.RemoveAll(u => u.UserName == user.UserName && u.SessionId == user.SessionId);
        }

        public static bool ValidateCurrentLogin(User user)
        {
            return user != null && _sessions.Any(u => u.UserName == user.UserName && u.SessionId == user.SessionId);
        }
    }

    public class User {
        public string UserName { get; set; }
        public string SessionId { get; set; }
    }

With this, during your login process after you have verified the user, you create an instance of the User class and assign it the username and session id, save it as a Session object, then call RegisterLogin function with it.

Then, on each page load, you get the session object and pass it to the ValidateCurrentLogin function.

The DeregisterLogin function is not strictly necessary, but keeps the _sessions object as small as possible.

Solution 5

I would like to point out that a key reason for setting Session["SessionID"] = "anything" is because until you actually assign something into the session object the session ID seems to keep changing on every request.

I ran into this with some split testing software I write.

Share:
62,285
Mike Marks
Author by

Mike Marks

My name is Mike and I work for a leading marketing cloud provider, headquartered in Indianapolis, but I work remote in Oklahoma. My experience lies in: ASP.NET C# MVC (3, 4, 5) WebAPI (1, 2) HTML JavaScript/jQuery (not an expert by any means) JSON MSSQL

Updated on September 27, 2021

Comments

  • Mike Marks
    Mike Marks over 2 years

    What I want to do is to limit a user ID to only being able to log in to one device at a time. For example, user ID "abc" logs in to their computer. User ID "abc" now tries to log in from their phone. What I want to happen is to kill the session on their computer.

    The Spotify app does exactly this- Spotify only allows one User ID to be logged in on one device at a time.

    I'm using ASP.NET membership (SqlMembershipProvider) and Forms Authentication.

    I've experimented with Session variables but I'm not sure exactly where to go from here.