How to detect and logout the user from an application if he logins from another browser/device in IdentityServer4?

1.4k views Asked by At

I have implemented the IdentityServer4 SSO in my application. SSO works fine as well as Logout for all the clients,However there is a new requirement where if the user is already logged in into an application and if he tries to login again (From different devise/browser) then he should be automatically logged out of the previous browser. I am not getting my head around this.How to implement this and if it is possible at all to track the user login sessions?

Update:-

We have tried following way of doing it, We have added the Session info into the Global Static variables using "Action" filter attribute.Here we stored the Login Session Info after user gets logged in.

      private class LoginSession
        {
            internal string UserId { get; set; }
            internal string SessionId { get; set; }
            internal string AuthTime { get; set; }
            internal DateTimeOffset AuthDateTime
            {
                get
                {
                    if (!string.IsNullOrEmpty(AuthTime))
                        return DateTimeOffset.FromUnixTimeSeconds(long.Parse(AuthTime));
                    else
                        return DateTimeOffset.UtcNow;
                }
            }
        }

        private static List<LoginSession> LoginSessions = new List<LoginSession>();

In "Action Filter" methods we check if the user's session id is already present or not. If the session is present and it's SessionId is not matching with claims session id then we check the Login time of the Session. If the login time is less than the current login time then the user is logged out of the system else we update the login session with the latest session id and login time. Due to this workflow for the second login the Login Session will be updated as the Login Time is always Greater than the saved Login Session Info. And for the old Logged in session the user will be logged out of the system as the login time would always be less than the updated session info.

public class SessionValidationAttribute : ActionFilterAttribute
{        
    public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    {
        string action = context.RouteData.Values["action"].ToString();

        if (!string.IsNullOrEmpty(action) &&
            context.Controller.GetType().GetMethod(action).GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Length == 0)
        {
            var claims = ((ClaimsIdentity)((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller).User.Identity).Claims;

            var sessionId = claims.Where(x => x.Type == "sid").First().Value; // context.HttpContext.Request.Cookies.TryGetValue("idsrv.session", out var sessionId);
            var userId = claims.Where(x => x.Type == "sub").First().Value;
            var authTime = claims.Where(x => x.Type == "auth_time").First().Value;
            var authDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(authTime));

            if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0) // if already logged in 
            {
                var latestLogin = LoginSessions.Where(x => x.UserId == userId).OrderByDescending(x => x.AuthDateTime).First();

                if (sessionId != latestLogin.SessionId)
                {
                   if(authDateTime > latestLogin.AuthDateTime) // login using new browser(session)
                   {
                       latestLogin.SessionId = sessionId; // assign latest sessionId
                       latestLogin.AuthTime = authTime; // assign latest authTime
                   }
                   else if (authDateTime < latestLogin.AuthDateTime) // login using old browser(session)
                   {
                     LoginSessions.RemoveAll(x => x.UserId == userId && x.SessionId!=latestLogin.SessionId);

                    context.Result = ((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller)
                                            .RedirectToAction(actionName: "Logout", controllerName: "Home",
                                            routeValues: new { tenant = string.Empty, isRemoteError = false });
                   }
                }
            }
            else
            {
                var newLogin = new LoginSession() { UserId = userId, SessionId = sessionId, AuthTime = authTime };
                LoginSessions.Add(newLogin);
            }
        }
        return base.OnActionExecutionAsync(context, next);
    }
}

This works as we tested for few users but Will this solution work in actual scenario where there are thousands of users login into the system?Is it a good idea to use Static variable globally for storing session info? What will be potential drawbacks of using this.Please advice. We are open to new ideas also,if there is any new methods of implementing this functionality please let us know.

Thanks!!!

1

There are 1 answers

0
php_nub_qq On

Disclaimer: I have no practical experience with IS4.

You probably have a good reason but I'm failing to understand why you are overwriting the latestLogin session's details when you are validating the current latest login?

If I'm not mistaken this line will loop through all sessions in your application, which you have multiple alike of in the lines that follow.

if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0)

This is indeed something you wouldn't want to do in an application you expect to scale.

Unfortunately I'm not familiar with IS4 and I can not tell you if there is a possibility to solve this problem entirely by utilizing its APIs, instead I can give you practical advice.

You can use a separate centralized storage, the possibilities are endless but something along the lines of memcached is perfect. Then the algorithm is fairly simple:

  1. Whenever a user tries to log in, retrieve the value stored under user's ID from storage.
  2. If present, that would be the current session ID, then destroy it in IS4 and continue.
  3. Create new login session and store the session ID in memcached under the user's ID.

This way there will never be more than 1 session for a given user and you've successfully reduced the complexity of the algorithm from O(n), or worse in your case, to O(1).