Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Prevent multiple logins

I am trying to block multiple logins with the same user in my application.
My idea is to update the security stamp when user signin and add that as a Claim, then in every single request comparing the stamp from the cookie with the one in the database. This is how I've implemented that:

        public virtual async Task<ActionResult> Login([Bind(Include = "Email,Password,RememberMe")] LoginViewModel model, string returnUrl)     {         if (!ModelState.IsValid)         {             return View(model);         }          SignInStatus result =             await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, false);         switch (result)         {             case SignInStatus.Success:                 var user = UserManager.FindByEmail(model.Email);                 var id = user.Id;                 UserManager.UpdateSecurityStamp(user.Id);                 var securityStamp = UserManager.FindByEmail(model.Email).SecurityStamp;                 UserManager.AddClaim(id, new Claim("SecurityStamp", securityStamp)); 

Then in authentication configuration I've added

        app.UseCookieAuthentication(new CookieAuthenticationOptions         {             AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,             LoginPath = new PathString("/Account/Login"),             Provider = new CookieAuthenticationProvider             {                 OnValidateIdentity = ctx =>                 {                     var ret = Task.Run(() =>                     {                         Claim claim = ctx.Identity.FindFirst("SecurityStamp");                         if (claim != null)                         {                             var userManager = new UserManager<ApplicationUser>(new UserStore<ApplicationUser>(new ApplicationDbContext()));                             var user = userManager.FindById(ctx.Identity.GetUserId());                              // invalidate session, if SecurityStamp has changed                             if (user != null && user.SecurityStamp != null && user.SecurityStamp != claim.Value)                             {                                 ctx.RejectIdentity();                             }                         }                     });                     return ret;                 }             }          }); 

As it shows I have tried to compare the claim from the cookie with the one in the database and reject the identity if they are not the same.
Now, each time the user signs in the security stamp gets updated but the value is different in user's cookie which I can't find out why? I am suspicious maybe it the new updated security stamp doesn't get stored in user's cookie?

like image 683
Shahin Avatar asked Aug 05 '15 11:08

Shahin


People also ask

How do I stop multiple logins from the same user?

To prevent the user from login on multiple systems or web browsers you need to generate a token on each successful login attempt. Need to check the token on each page. If the token does not match then destroy the SESSION and log out the user.

How do I prevent different users from logging into other devices?

Use device id and auth token both. At time of login save device id along with a token. and every time check if user is already logged in on any device, if yes then delete old token and generate new token.


1 Answers

The solution is somewhat more simple than you have started implementing. But the idea is the same: every time user logs in, change their security stamp. And this will invalidate all other login sessions. Thus will teach users not to share their password.

I have just created a new MVC5 application from standard VS2013 template and successfully managed to implement what you want to do.

Login method. You need to change the security stamp BEFORE you create auth cookie, as after the cookie is set, you can't easily update the values:

[HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task<ActionResult> Login(LoginViewModel model, string returnUrl) {     if (!ModelState.IsValid)     {         return View(model);     }       // check if username/password pair match.     var loggedinUser = await UserManager.FindAsync(model.Email, model.Password);     if (loggedinUser != null)     {         // change the security stamp only on correct username/password         await UserManager.UpdateSecurityStampAsync(loggedinUser.Id);     }       // do sign-in     var result = await SignInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, shouldLockout: false);     switch (result)     {         case SignInStatus.Success:             return RedirectToLocal(returnUrl);         case SignInStatus.LockedOut:             return View("Lockout");         case SignInStatus.RequiresVerification:             return RedirectToAction("SendCode", new { ReturnUrl = returnUrl, RememberMe = model.RememberMe });         case SignInStatus.Failure:         default:             ModelState.AddModelError("", "Invalid login attempt.");             return View(model);     } } 

This way every login will do an update on the user record with the new security stamp. Updating security stamp is only a matter of await UserManager.UpdateSecurityStampAsync(user.Id); - much simplier than you imagined.

Next step is to check for security stamp on every request. You already found the best hook-in point in Startup.Auth.cs but you again overcomplicated. The framework already does what you need to do, you need to tweak it slightly:

app.UseCookieAuthentication(new CookieAuthenticationOptions {     // other stuff     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,     LoginPath = new PathString("/Account/Login"),     Provider = new CookieAuthenticationProvider     {         OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(             validateInterval: TimeSpan.FromMinutes(0), // <-- Note the timer is set for zero             regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))     } });             

The time interval is set for zero - means the framework on every request will compare user's security stamp with the database. If stamp in the cookie does not match the stamp in the database, user's auth-cookie is thrown out, asking them to logout.

However, note that this will bear an extra request to your database on every HTTP request from a user. On a large user-base this can be expensive and you can somewhat increase the checking interval to a couple minutes - will give you less requests to your DB, but still will carry your message about not sharing the login details.


Full source in github


More information in a blog-post

like image 61
trailmax Avatar answered Sep 21 '22 09:09

trailmax