40

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.

abatishchev
  • 98,240
  • 88
  • 296
  • 433
Mike Marks
  • 10,017
  • 17
  • 69
  • 128
  • 5
    Yes, my idea is that I would never use your app. There are way too many situations in which that can backfire. And, since there is no for sure way to know when someone is logged in or not, you can force legitimate users to have to wait for timeouts to expire before they can login from another device. I understand your concerns about protecting your content, but I refuse to use any service which does this because I am always frustrated by them. – Erik Funkenbusch Apr 08 '13 at 19:34
  • 1
    However, having said that.. No, there is no built-in way to do this, because a) it's a seldom needed feature and b) because it's impossible to do reliably without annoying lots of people. – Erik Funkenbusch Apr 08 '13 at 19:36
  • are you using forms authentication or the WebMatrix classes? – Rob Apr 08 '13 at 19:39
  • There is no "out of the box" implementation of this. You would have to manually keep track of who logs in and when the same username makes any request, manually "timeout" the other sessions. – Rob Apr 08 '13 at 19:47
  • Rather than disallowing concurrent signins, guard your services. Which means, if pages / web services are invoked by an authenticated user, log this somewhere and check if the service hasn't been lately invoked from another session. – Wiktor Zychla Apr 08 '13 at 19:53

6 Answers6

42

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.

Mike Marks
  • 10,017
  • 17
  • 69
  • 128
  • 3
    You may want rewrite that snippet as `return logins.Any(i => i.LoggedIn == true && i.UserId == userId && i.SessionId != sid);` – abatishchev Jul 08 '13 at 03:35
  • Yeah I love comments to 2-3 years old posts too :-D – abatishchev Jul 08 '13 at 03:36
  • @abatishchev Thanks for the suggestion... btw, this post is only a few months old! :) – Mike Marks Jul 08 '13 at 12:29
  • 3
    @MikeMarks If you have working samples share it also. it will be useful for many. – Pandiyan Cool Sep 25 '13 at 10:30
  • 1
    What happen when a user closes his browser and come back being automatically logged from its authentication cookie ? I think a new session will be created but its id will never be in the Logins table as it didn't go through the Account controller. How do you manage this case ? – Michaël Carpentier Nov 04 '13 at 15:30
  • @MichaëlCarpentier Good question... let me think on that.. I'll get back to you. – Mike Marks Nov 04 '13 at 15:42
  • @MichaëlCarpentier I do want to say off hand that when a user navigates to any part of your app that has the "checks" in place at the controller level, each page makes sure (via the controller code) that the user ID and session ID with LoggedIn = True exists in the Logins table, OTHERWISE, they are redirected to the Login page. So in your example, if a user closes out their browser and reopens under a different session, and they try to go to, say, ProductsController, the controller will check to see if their ID and SessionID exist in the Logins table (with LoggedIn=True). It won't in this... – Mike Marks Nov 04 '13 at 15:46
  • @MichaëlCarpentier It won't in this case, so the user that just opened up a new browser session (they didn't go through the account controller like you said) will get redirected to the Login page because they don't have a valid record in the Logins table. If their session WAS the same session ID, then the code would be like "hey, this person exists in the Logins table under that session ID, and LoggedIn is still True, so let them pass", while logging all the other records for that particular UserId out (under different sessions, if they exist) – Mike Marks Nov 04 '13 at 15:47
  • 1
    @MikeMarks No problem if the new session id is the same you're right. But if not, the login "Remember me" feature becomes useless. – Michaël Carpentier Nov 04 '13 at 16:09
  • @MichaëlCarpentier You are correct... for the specific requirements I had, my customer didn't want to make use of the "Remember Me" functionality. So, I created this solution not considering it. I'm pretty sure though that with some additional code it'd be able to be implemented into my solution. – Mike Marks Nov 04 '13 at 16:12
  • 2
    @MikeMarks That was precisely the subject of my question. I'm looking for a place where I can hook that "automatic login" and add the code to create the Logins row. Perhaps someone else can help ? Thanks for your quick answers anyway. – Michaël Carpentier Nov 04 '13 at 16:30
  • What is `CapWorxQuikCapContext`? I can't seem to find any references to it anywhere. – Abe Miessler Sep 30 '14 at 19:53
  • 2
    I have a similar implemention, upon login I insert/update a GUID and LoginName into a table. The guid is then set as a cookie. In my web I then have a polling function which checks periodically if the user is still valid. If the Guids don't match he's invalid, if the table doesn't have the login, I just assume he's valid. This is meant to prevent duplicate logins not for authentication/authorization purposes. Now that I'm using signalr, I'll enhance this so upon login it can kick out the other user without polling the server. – JoshBerke Sep 30 '14 at 20:35
  • @MikeMarks what to do if session gets Expired ? how database will know ? – Dgan Aug 23 '16 at 08:19
  • I'm fighting with the "Remember Me" issue too. What if I set LoggedIn = false inside Session_End? Wouldn't it work, @MichaëlCarpentier? Then we can add an extra checking to the controller code (actually, I have implemented an ActionFilter): If ( !IsUserLoggedAnyWhere ) Add this session as if it were a new login. – Jaime Aug 24 '16 at 14:45
  • why we have a field as "LoggedIn"? what if we directly delete other locations' rows ,do we miss anything? I it would be effective, keeping less rows in table – Sessiz Saat Dec 08 '16 at 09:31
  • Can you please tell me what did you mean by `OperationContext` ? which is inbuild class in c# https://learn.microsoft.com/en-us/dotnet/api/system.servicemodel.operationcontext?view=netframework-4.7.2. So should I need to implement those three methods inside that class or need to create another class to implement those three methods? `OperationContext` is mandatory? – Sachith Wickramaarachchi Nov 14 '18 at 03:56
  • @MikeMarks why did you logout every one else on Index action. Isn't it better to logout during the login process instead of logout on the index page? – brtb Jun 12 '19 at 12:18
  • How to Disable concurrent sessions. Logout previous sessions if user logs in new system. – Balasubramanian Ramamoorthi Oct 11 '21 at 14:12
8

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.

Erik Funkenbusch
  • 92,674
  • 28
  • 195
  • 291
  • And this is assuming when the user logs out, remove their record from the database (session), right? – Mike Marks Apr 09 '13 at 16:12
  • @MikeMarks - No, you don't need to remove any records. So long as you only want to allow one. You just keep whatever the most recent login session id is, and bounce anyone with a different session ID. – Erik Funkenbusch Apr 09 '13 at 16:39
  • Working with `SecurityStamp` is more reliable then in this case. Like if you are checking on each request then why not writing security stamp on each login. and check that stamp every time a users requests to server? Have a look here: https://tech.trailmax.info/2015/09/prevent-multiple-logins-in-asp-net-identity/ – Jamshaid K. Jan 14 '19 at 15:39
7

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.

Darin Dimitrov
  • 1,023,142
  • 271
  • 3,287
  • 2,928
  • 1
    Since new sessions supersede existing sessions, you don't *need* to do anything on logout/for expiry - when they next establish a new session, it wipes out the old one. – Damien_The_Unbeliever Apr 09 '13 at 13:52
  • This would assume proper removal of records once the user "logs out", right? Otherwise, the table will show all sessions, expired and new. – Mike Marks Apr 09 '13 at 16:10
  • @MikeMarks, no you don't need to do anything special on logout. As Damien already explained the new session will simply overseed the old ones from the same user. – Darin Dimitrov Apr 09 '13 at 21:10
3

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.

Kevin
  • 7,162
  • 11
  • 46
  • 70
1

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.

Roger Willcocks
  • 1,649
  • 13
  • 27
0

I would handle this situation by creating an array of sessionId of a user, and activeSessionId variable which would store the sessionId of the device with the most recent successful request. Then on each request, I would check:

  • If the sessionId of the device which made this request exists and is valid.
  • If it is not the same as the activeSessionId.

If yes, then I know that this request is from different device and hence I would log out the user from previous device and update activeSessionId with that of current device.

Toni
  • 1,555
  • 4
  • 15
  • 23