What I need to do
I'm developing an application using ASP.NET CORE and I actually encountered a problem using the Identity implementation.
In the official doc infact there is no reference about the multiple session, and this is bad because I developed a SaaS application; in particular a user subscribe a paid plan to access to a specific set of features and him can give his credentials to other users so they can access for free, this is a really bad scenario and I'll lose a lot of money and time.
What I though
After searching a lot on the web I found many solutions for the older version of ASP.NET CORE, so I'm not able to test, but I understood that the usually the solution for this problem is related to store the user time stamp (which is a GUID generated on the login) inside the database, so each time the user access to a restricted page and there are more session (with different user timestamp) the old session will closed.
I don't like this solution because an user can easily copy the cookie of the browser and share it will other users.
I though to store the information of the logged in user session inside the database, but this will require a lot of connection too.. So my inexperience with ASP.NET CORE and the lack of resource on the web have sent me in confusion.
Someone could share a generic idea to implement a secure solution for prevent multiple user login?
4 Answers
Answers 1
After searching a lot on the web I found many solutions for the older version of ASP.NET CORE, so I'm not able to test, but I understood that the usually the solution for this problem is related to store the user time stamp (which is a GUID generated on the login) inside the database, so each time the user access to a restricted page and there are more session (with different user timestamp) the old session will closed.
This is exactly what I do. My Users table has a SessionId and on every login the SessionId gets overwritten with a new Id. This session Id is checked on every request via a default policy in your authorization settings.
Create classes to validate the session.
public class ValidSessionRequirement : IAuthorizationRequirement { } public class ValidSessionHandler : AuthorizationHandler<ValidSessionRequirement> { private readonly ApplicationDbContext _context; private readonly ClaimsPrincipal _user; public ValidSessionHandler(ApplicationDbContext context, ClaimsPrincipal user) { _context = context; _user = user; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidSessionRequirement requirement) { if (!context.User.Identity.IsAuthenticated) return; // get user from database var user = _context.Users.FirstOrDefault(e => e.Username == _user.Identity.Name); // get session Id from user claims var sessionId = _user.FindFirstValue("sessionId"); // if user isn't null and the session ID is the same then suceed the requirement if (user != null && user.SessionId == sessionId) { context.Succeed(requirement); } return; } } Configure your validation requirement in authorization settings
var defaultPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddRequirements(new ValidSessionRequirement()) .Build(); services.AddAuthorization(options => { options.DefaultPolicy = defaultPolicy; }); services.AddHttpContextAccessor(); builder.Services.AddTransient(provider => provider.GetService<IHttpContextAccessor>().HttpContext.User); services.AddTransient<IAuthorizationHandler, ValidSessionHandler>(); So when you login, your other session is terminated. I prefer to do it this way so people don't have to worry about logging out of other devices. They can just overwrite the session each time. If two people are both using the same credentials, they will keep kicking each other out of the system.
Though, I'm using JWT token authentication and not cookie, so your implementation may be a little different.
Answers 2
You can use UpdateSecurityStamp to invalidate any existing authentication cookies. For example:
public async Task<IActionResult> Login(LoginViewModel model) { var user = await _userManager.FindByEmailAsync(model.Email); if (user == null) { ModelState.AddModelError(string.Empty, "Invalid username/password."); return View(); } if (await _userManager.ValidatePasswordAsync(user, model.Password)) { await _userManager.UpdateSecurityStampAsync(user); var result = await _signInManager.SignInAsync(user, isPersistent: false); // handle `SignInResult` cases } } By updating the security stamp will cause all existing auth cookies to be invalid, basically logging out all other devices where the user is logged in. Then, you sign in the user on this current device.
Answers 3
Best way is to do something similar to what Google, Facebook and others do -- detect if user is logging in from a different device. For your case, I believe you would want to have a slight different behavior -- instead of asking access, you'll probably deny it. It's almost like you're creating a license "per device", or a "single tenant" license.
This Stack Overflow thread talks about this solution.
The most reliable way to detect a device change is to create a fingerprint of the browser/device the browser is running on. This is a complex topic to get 100% right, and there are commercial offerings that are pretty darn good but not flawless.
Note: if you want to start simple, you could start with a Secure cookie, which is less likely to be exposed to cookie theft via eavesdropping. You could store a hashed fingerprint, for instance.
Answers 4
There are some access management solutions (ForgeRock, Oracle Access Management) that implement this Session Quota functionality. ForgeRock has a community version and its source code is available on Github, maybe you can take a look at how it is implemented there. There is also a blog post from them giving a broad view of the functionality (https://blogs.forgerock.org/petermajor/2013/01/session-quota-basics/)
If this is too complex for your use case, what I would do is combine the "shared memory" approach that you described with an identity function, similar to what Fabio pointed out in another answer.
0 comments:
Post a Comment