How to prevent multiple login in SAAS application? - asp.net-core

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?

I've created a github repo with the changes to the default .net core 2.1 template needed to only allow single sessions. https://github.com/xKloc/IdentityWithSession
Here is the gist.
First, override the default UserClaimsPrincipalFactory<IdentityUser> class with a custom one that will add your session to the user claims. Claims are just a key/value pair that will be stored in the user's cookie and also on the server under the AspNetUserClaims table.
Add this class anywhere in your project.
public class ApplicationClaimsPrincipalFactory : UserClaimsPrincipalFactory<IdentityUser>
{
private readonly UserManager<IdentityUser> _userManager;
public ApplicationClaimsPrincipalFactory(UserManager<IdentityUser> userManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, optionsAccessor)
{
_userManager = userManager;
}
public async override Task<ClaimsPrincipal> CreateAsync(IdentityUser user)
{
// find old sessions and remove
var claims = await _userManager.GetClaimsAsync(user);
var session = claims.Where(e => e.Type == "session");
await _userManager.RemoveClaimsAsync(user, session);
// add new session claim
await _userManager.AddClaimAsync(user, new Claim("session", Guid.NewGuid().ToString()));
// create principal
var principal = await base.CreateAsync(user);
return principal;
}
}
Next we will create an authorization handler that will check that the session is valid on every request.
The handler will match the session claim from the user's cookie to the session claim stored in the database. If they match, the user is authorized to continue. If they don't match, the user will get a Access Denied message.
Add these two classes anywhere in your project.
public class ValidSessionRequirement : IAuthorizationRequirement
{
}
public class ValidSessionHandler : AuthorizationHandler<ValidSessionRequirement>
{
private readonly UserManager<IdentityUser> _userManager;
private readonly SignInManager<IdentityUser> _signInManager;
public ValidSessionHandler(UserManager<IdentityUser> userManager, SignInManager<IdentityUser> signInManager)
{
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
_signInManager = signInManager ?? throw new ArgumentNullException(nameof(signInManager));
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, ValidSessionRequirement requirement)
{
// if the user isn't authenticated then no need to check session
if (!context.User.Identity.IsAuthenticated)
return;
// get the user and session claim
var user = await _userManager.GetUserAsync(context.User);
var claims = await _userManager.GetClaimsAsync(user);
var serverSession = claims.First(e => e.Type == "session");
var clientSession = context.User.FindFirst("session");
// if the client session matches the server session then the user is authorized
if (serverSession?.Value == clientSession?.Value)
{
context.Succeed(requirement);
}
return;
}
}
Finally, just register these new classes in start up so they get called.
Add this code to your Startup class under the ConfigureServices method, right below services.AddDefaultIdentity<IdentityUser>()
.AddEntityFrameworkStores<ApplicationDbContext>();
// build default authorization policy
var defaultPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.AddRequirements(new ValidSessionRequirement())
.Build();
// add authorization to the pipe
services.AddAuthorization(options =>
{
options.DefaultPolicy = defaultPolicy;
});
// register new claims factory
services.AddScoped<IUserClaimsPrincipalFactory<IdentityUser>, ApplicationClaimsPrincipalFactory>();
// register valid session handler
services.AddTransient<IAuthorizationHandler, ValidSessionHandler>();

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.

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.

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.

Related

Custom Authorizationhandler for token evaluation that is done externally

When the user submits his credentials to my api, I call an external api to authenticate the user. After that, a token gets generated on the external api and will be sent to me. For that I implemented the HandleAuthenticateAsync function from the AuthenticationHandler:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
//before this: make call to external api to get the access token
var claims = new[] {
new Claim(ClaimTypes.Name, submittedToken),
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
I have implemented a custom AuthorizationHandler which I want to check for the access token that you got when you successfully authenticate. Note that the actual authentication and authorization is done by an external api which is a custom implementation. Here is the function:
public class IsAuthorizedRequirement : AuthorizationHandler<IsAuthorizedRequirement>, IAuthorizationRequirement
{
public AuthenticateHandlerHelperFunctions AuthenticateHandlerHelper;
public IsAuthorizedRequirement()
{
AuthenticateHandlerHelper = new AuthenticateHandlerHelperFunctions();
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, IsAuthorizedRequirement requirement)
{
if(!context.User.HasClaim(c => c.Type == ClaimTypes.Name))
{
context.Fail();
return;
}
var token = context.User.FindFirst(c => c.Type == ClaimTypes.Name).Value;
if (!string.IsNullOrEmpty(token))
{
context.Fail();
return;
}
var checkedToken = await AuthenticateHandlerHelper.CheckAccessToken(token);
if (checkedToken == null)
{
context.Fail();
return;
}
context.Succeed(requirement);
}
}
The CheckAccessToken function makes a simple HTTP Post Request to the external Api where I get back if the token is still valid or not. Is this a valid implementation especially when multiple users are using this? Especially the claims that I use: Are they created for each user or will the content inside ClaimsType.Name be overwritten each time a user makes a request? Currently I have no way to test this so I just wanted to know if I am on the right track for this. Thanks
Is this a valid implementation especially when multiple users are using this?
I strongly stand against this approach. Implementation like this mean you would call external API for validate and generate token(or cookie or any form of authenticated certificate) on external server for each and any of your request(which require authentication).
It's could be consider acceptable if we have some special cases on just some endpoints. But for the whole API/Web server. Please don't use this approach.
Especially the claims that I use: Are they created for each user or will the content inside ClaimsType.Name be overwritten each time a user makes a request?
They'll create for each request. As I can see in the code there are no part for generate cookie or some form of retaining user information for the client to attach next request afterward.

.Net Core 3.1 ClaimsTransformation Manually Added Claims Not Persisting

I will be accessing several tables to determine if a user is "Validated" or not as well as adding custom roles to a Windows authenticated user for authorization. For now I'm running a test in a basic .net Core web application just to see how I should be doing this. I have setup a RequiredClaim in my Fallback Policy and a ClaimsLoader and it works great:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
services.AddTransient<IClaimsTransformation, ClaimsLoader>();
services.AddAuthentication(IISDefaults.AuthenticationScheme);
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireClaim("ValidatedUser")
.Build();
});
}
public class ClaimsLoader : IClaimsTransformation
{
public async Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
{
var claimsIdentity = (ClaimsIdentity)principal.Identity;
claimsIdentity.AddClaim(new Claim("ValidatedUser", ""));
return await Task.FromResult(principal);
}
}
As long as that AddClaim line is in there, they can access the app, without it they get a not-authorized response which is what I want.
Based on what I've read I thought any claims/roles I add in the transformation should come back each time but they do not. In the code above I have the AddClaim running every time so it's working, but in reality I will be going to a database to determine if I should add that claim which is an expensive process. I want to persist the results across multiple requests. So I want to check if the claim is already there and not bother getting it again if it is. For whatever reason it is NEVER there when it comes back for a second request.
From what I've read here back in 2.x the claims should persist:
https://philipm.at/2018/aspnetcore_claims_with_windowsauthentication.html
But here in my 3.1 application they do not.

Create shorter tokens with small lifespan in ASP.NET Core Identity

Using ASP.NET Core 3.1 I am creating an User's Email confirmation token to send by email:
String token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
And I get the following:
CfDJ8IjJLi0iO61KsS5NTyS4wJkSvCyzEDUBaVlXCkbxz6zwI1LocG8+WPubx5Rvoi4tFuiWAVFut4gfTnhgsdihE0gY+o7JyJrNtfXmzGLnczwbKZ3Wwy15+IUEi1h2qId72IRKvFqBSFv7rJdECSR/thZphpTQm7EnOuAA7loHlQFRWuMUVBce8HUsv1odbLNsKQ==
How can I create shorter tokens with a small lifespan instead of huge tokens?
If I understand the problem, you're looking at swapping out a TokenProvider, which can either be done at service container configuration stage
TokenProvider.cs
public class TokenProvider : IUserTwoFactorTokenProvider<IdentityUser>
{
public Task<string> GenerateAsync(string purpose, UserManager<IdentityUser> manager, IdentityUser user)
{
// generate your token here
}
public Task<bool> ValidateAsync(string purpose, string token, UserManager<IdentityUser> manager, IdentityUser user)
{
// validate your token here
}
public Task<bool> CanGenerateTwoFactorTokenAsync(UserManager<IdentityUser> manager, IdentityUser user)
{
// check if user has email and it's been confirmed. or do your own logic
}
}
inject into your container at build time
services.AddIdentityCore<IdentityUser>(o =>
{
o.Tokens.EmailConfirmationTokenProvider = "MyTokenProvider";
}).AddEntityFrameworkStores<IdentityDbContext>()
.AddTokenProvider<TokenProvider>("MyTokenProvider");
or at run time:
_userManager.RegisterTokenProvider(um.Options.Tokens.ChangeEmailTokenProvider, new TokenProvider());
String token = await _userManager.GenerateEmailConfirmationTokenAsync(user);
there are a few token providers available to you by default (Email, PhoneNumber and Authenticator being some), which you can explore and build upon. As far as I can see the source, EmailTokenProvider defers actual code generation to TotpSecurityStampBasedTokenProvider which you can explore and see if your lifetime requirement can be changed by playing with the TOTP algorithm it implements
Lifespan doesn't factor in here either way. However, I think what you're actually talking about is an TOTP (timed one-time use password) - like the ones you get via SMS or an authenticator app. ASP.NET Core actually has TOTP providers built-in; they're just not used for things like email confirmation, password reset, etc. by default. However, that's easily changed:
services.Configure<IdentityOptions>(o =>
{
o.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
Oddly enough, despite being called DefaultEmailProvider, that provider is not actually used by default for things like email confirmations. It's actually referring to being the default TOTP provider for 2FA codes delivered via email. Nevertheless, you can set it as the provider for email confirmation, as well.

Openiddict. Save custom claim to AspNetUserClaims table

I have an auth server based on openiddict 2.0. What I need is to add a custom claim. After some searching, I came to custom implementation of UserClaimsPrincipalFactory:
public class CustomUserClaimsPrincipalFactory : UserClaimsPrincipalFactory<ApplicationUser, IdentityRole>
{
public CustomUserClaimsPrincipalFactory(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
IOptions<IdentityOptions> optionsAccessor)
: base(userManager, roleManager, optionsAccessor)
{
}
protected override async Task<ClaimsIdentity> GenerateClaimsAsync(ApplicationUser user)
{
var customClaim = new Claim("demo", "some value");
var identity = await base.GenerateClaimsAsync(user);
identity.AddClaim(customClaim);
return identity;
}
}
It works and "demo" claim is added to token and is available in User.Claims from the controller action (I am using OAuth2 validation middleware). The only one thing that is confusing is that record with that claim is not added to AspNetUserClaims table. I have found that I can "fix" this by adding it directly to GenerateClaimsAsync method via:
await UserManager.AddClaimAsync(user, customClaim);
and now I am wondering is it correct approach, or there is some simple configuration step, maybe something like "option" flag that I simply need to enable during openiddict setup...
You are adding a claim at two different levels.
UserManager addresses the store. It looks up and saves data in the database, allowing an async implementation. The ClaimsIdentity on the other hand is not connected to the store.
Though both have an AddClaim method, the UserManager actually persists the data, while the ClaimsIdentity doesn't.
The idea of persisted claims is, that once persisted, the claims should be added automatically to the ClaimsIdentity for you. You may have to implement this yourself.

ASP Core Add Custom Claim to Auth Token

I'm using openiddict and ASP Identity, and I am trying add a "GroupId" as a claim to the auth token that is returned when logging in (using the /connect/token endpoint - see example I followed below). The GroupId is a property in my AplicationUser class.
I have tried using an IClaimsTransformer but that seems clunky, I can't easily get to the UserManager from the ClaimsTransformationContext.
How would I go about either getting the UserManager through DI in my IClaimsTransformer or just adding the GroupId to the token that is generated at the connect/token endpoint?
I followed this example for setting up my site. This is what I would like to do:
var groupGuid = User.Claims.FirstOrDefault(c => c.Type == "GroupGuid");
There is a couple of ways to achieve it:
First, override CreateUserPrincipalAsync method in your custom SignInManager:
public override async Task<ClaimsPrincipal> CreateUserPrincipalAsync(ApplicationAdmin user)
{
var principal = await base.CreateUserPrincipalAsync(user);
// use this.UserManager if needed
var identity = (ClaimsIdentity)principal.Identity;
identity.AddClaim(new Claim("MyClaimType", "MyClaimValue"));
return principal;
}
The second way is to override CreateAsync method of your custom UserClaimsPrincipalFactory:
public override async Task<ClaimsPrincipal> CreateAsync(ApplicationUser user)
{
var principal = await base.CreateAsync(user);
var identity = (ClaimsIdentity)principal.Identity;
identity.AddClaim(new Claim("MyClaimType", "MyClaimValue"));
return principal;
}
which is, basically, the same, because base.CreateUserPrincipalAsync method in SignInManager calls this.UserClaimsPrincipalFactory() inside.
Don't forget to add your custom implementations into services:
either
public void ConfigureServices(IServiceCollection services)
{
...
services.AddSignInManager<CustomSignInManager>();
}
or
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IUserClaimsPrincipalFactory<ApplicationUser>, CustomClaimsPrincipalFactory>();
}
ASP Core Add Custom Claim to Auth Token
You can't change a token after it is signed by IdP, so you can't add claim to token.
I have tried using an IClaimsTransformer but that seems clunky, I
can't easily get to the UserManager from the
ClaimsTransformationContext.
I guess your problem is related this github issue. In summary(as far as i understand) if ClaimsTransformer class is registered as singletion and one of the its dependency is scoped or transient, it causes captive dependency. In this case you should use Service Locator pattern to avoid from captive dependency. Your code may be something like this(from #PinpointTownes' comment):
public async Task<ClaimsPrincipal> TransformAsync(ClaimsTransformationContext context)
{
var userManager= context.Context.RequestServices.GetRequiredService<UserManager>();
//
}
-- My thoughts about your case --
You have basically two options to achieve your goal:
Add claim to token when token is generated by IdP:
You don't need this method most cases, but if you want to use it:
You should have control over IdP, because this option is possible on the IdP(as far as i understand your IdP and Resource Server is same, so you have control over IdP, but it might not be possible always) .
You should take care of inconsistency when using this option, because the claim is stored in the token and doesn't get each request. So the real value of claim might be different from claim in the token.(I don't prefer it for roles, permissions, groups etc. because these claims can be change anytime).
p.s: i don't know if it is possible to add claims to token with Openiddict.
Claims Transformation
Actually i used HttpContext.Items to store additional claims before i discovered this method and it worked well for me. But i think better way is to use Claims Transformation and it fits into your case.