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!!!