IdentityServer4 Revoke all reference tokens for a client, except the current one - asp.net-core

I have a single page application which is protected by IdentityServer4 reference tokens.
I expect users to login to from multiple computers/devices.
In the settings area of the app, the user can change their password. To do so, they must enter their current password, as well as the new password.
I also wish to give the user the option to "Logout all other devices and computers".
If the user ticks this option, I want to invalidate any other reference tokens that exist for this client and this user, but I do NOT want to invalidate the reference token the user is currently using.
I only want it to logout other devices and computers. The user should stay logged in on the computer they are using.
For the life of me, I cannot see a way to do this with IdentityServer4. I was thinking I could simply run a delete on the PersistedGrants table, however I have no way of knowing which of the persisted grants in this table is the one the user is currently using.
Please help!

I was finally able to solve this. Make sure you're using the latest version of IdentityServer, as it includes a session_id column on the PersistedGrants table. With that, the solution is clear.
When user changes password:
if (model.EndSessions)
{
var currentSessionId = User.FindFirst(JwtClaimTypes.SessionId).Value;
foreach (var grant in db.PersistedGrants.Where(pg => pg.ClientId == "the-client-name" && pg.SubjectId == user.Id.ToString() && pg.SessionId != currentSessionId).ToList())
{
db.PersistedGrants.Remove(grant);
}
db.SaveChanges();
await userManager.UpdateSecurityStampAsync(user);
}
The user's other tokens are now revoked.
However, the user (on their other computer/devices) will likely still have an authentication cookie, so if they were to go to the authorization endpoint they would be granted a new token without having to login again.
To prevent that, we intercept the request for a new token with a CustomProfileService, like so -
public override async Task IsActiveAsync(IsActiveContext context)
{
//only run check for cookie authentication
if (context.Subject.Identity.AuthenticationType == IdentityConstants.ApplicationScheme)
{
var validationResponse = await signInManager.ValidateSecurityStampAsync(context.Subject);
if (validationResponse == null)
{
context.IsActive = false;
return;
}
var user = await userManager.GetUserAsync(context.Subject);
context.IsActive = user.IsActive;
}
}

Related

How to invalidate a JWT token of a user after password is changed by someone

I created an MVC page used JWT authentication. Once a user logged in successfully, the app returns a JWT token to the user that's stored in the request header. But then a problem occurred. Another person also signs in with the same user account and changes the password. So the first logged in session should be terminated because of security issues. The solution I thought is invalidating the JWT token of that user. But I have to define when was the user's password changed. The JWT token doesn't contain the password information so I couldn't request to the backend server to determinate the password was changed every time the user (with old password) request to the server, either. I need some ideas, suggestions.
For this feature you should add new property like SerialNumber as string on Users table
public class User { public string SerialNumber { get; set; } }
when you want to create new token for user add user SerialNumber to Claims like this
new Claim(ClaimTypes.SerialNumber, user.SerialNumber, ClaimValueTypes.String, issuer),
and when changed user password or username or status or every important property you should update serial number. when serial changed on token validator method after first http request will raise error code 401 (that means Unauthorized)
public async Task ValidateAsync(TokenValidatedContext context)
{
var claimsIdentity = context.Principal.Identity as ClaimsIdentity;
if (claimsIdentity?.Claims == null || !claimsIdentity.Claims.Any())
{
context.Fail("This is not our issued token. It has no claims.");
return;
}
var serialNumberClaim = claimsIdentity.FindFirst(ClaimTypes.SerialNumber);
if (serialNumberClaim == null)
{
context.Fail("This is not our issued token. It has no serial.");
return;
}
var userIdString = claimsIdentity.FindFirst(ClaimTypes.UserData).Value;
if (!int.TryParse(userIdString, out int userId))
{
context.Fail("This is not our issued token. It has no user-id.");
return;
}
var user = await _signInService.GetUserAsync(userId);
if (user == null)
{
context.Fail("User deleted!");
return;
}
if (user.SerialNumber != serialNumberClaim.Value || !user.Status)
{
context.Fail("This token is expired. Please login again.");
return;
}
}
on JWT Token configuration
OnTokenValidated = context =>
{
var tokenValidatorService = context.HttpContext.RequestServices.GetRequiredService<ITokenFactoryService>();
return tokenValidatorService.ValidateAsync(context);
},
Using the password directly in the token is a security risk, since an attacker could retrieve it from the user's computer. Better to either:
include a unique token ID in the token, and maintain a list of revoked tokens (or of allowed tokens; whichever is a better fit). or
include a "version number" in the user list, change it whenever the password is changed, and include the version number when the token is issued. That way, all old tokens can be rejected. #Mohammad's answer has an example of something similar.
None of those pieces of information means anything by themselves.
Use password as claim in jwt?
It will be checked in every request, so after password is changed it will return 401

Logging out the user from other computers (that he logged in before) when he logs in from another computer

I have a web application that employees log in to do stuff. What Im trying to achieve is: When a user logs in from a computer, if he is already logged in on another computer, that must be logged out. The web app is MVC Asp.Net Core 2.2 Code first. I have added a signInManager in startup and edited the PasswordSignInAsync method. I login the system from two different devices. When I click something on the screen from the first computer that I loggedin, it redirects to logout. It seems like working. But Im not sure if this is the right way of doing this. The code I added is: await UserManager.UpdateSecurityStampAsync(user); Inside PasswordSignInAsync method.
Inside the startup class ConfigureServices method I added
'services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddSignInManager<SignInManagerX>()'
Then in SignInManagerX class which is inherited from SignInManager I overrided the PasswordSignInAsync
public override async Task<SignInResult>
PasswordSignInAsync(ApplicationUser user, string password,
bool isPersistent, bool lockoutOnFailure)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
var attempt = await CheckPasswordSignInAsync(user, password,
lockoutOnFailure);
//Here is added
if (attempt.Succeeded)
{
await UserManager.UpdateSecurityStampAsync(user);
}
//Add end
return attempt.Succeeded
? await SignInOrTwoFactorAsync(user, isPersistent)
: attempt;
}
Is this the right way ? or I should add a table to db for logins which holds the info if the user is already logged in on another Ip. Then Logging out that user from all computers if the last and current login attempt is true ?
Yes , the primary purpose of the SecurityStamp is to enable sign out everywhere.
The basic idea is that whenever something security related is changed on the user, like a password, it is a good idea to automatically invalidate any existing sign in cookies, so if your password/account was previously compromised, the attacker no longer has access.
Reference : https://stackoverflow.com/a/19505060/5751404
You can set validateInterval to TimeSpan.Zero for immediate logout .

How to sign out previous login on new login in .net core?

How to sign out previous login when user log in through another browser in .net core?
I referred to this link but confused about how to use it.
enter link description here
You simply call UpdateSecurityStampAsync on your UserManager instance with the user in question. Then sign them in. This won't automatically log out other sessions, because there's a client-side component that must come into play. However, on the next request made from another browser, the cookie there will be invalidated because the security stamp won't match, and then the user will be effectively logged out.
It worked for me doing like:
After login done:
var loggedinUser = await _userManager.FindByEmailAsync(model.Email);
if (loggedinUser != null)
{
var Securitystamp = await _userManager.UpdateSecurityStampAsync(loggedinUser);
}
and in StartUp.cs
services.Configure<SecurityStampValidatorOptions>(options => options.ValidationInterval = TimeSpan.FromSeconds(0));

Store password in Keycloak

I have created a custom user storage provider which will migrate users from legacy system to keycloak's local storage on demand basis.
All the details of the migrated user is being stored in Keycloak except password.
userModel.setEmail(email);
userModel.setEnabled(isEnabled);
userModel.setEmailVerified(isEmailVerified);
userModel.setFirstName(firstName);
userModel.setLastName(lastName);
I am using the above code to store all the information of the user, but I didn't find any method/class in which stores the password.
Can anyone please help me with it?
P.S. I am using Keycloak-3.3.0-Final version.
You can use
session.userCredentialManager().updateCredential(realm, user, UserCredentialModel.password(passwordNew, false));
where session is the current KeycloakSession which you have access to in your custom user storage provider.
Thanks to Boomer's answer I managed to make it work in my implementation where the isValid function - which sends the POST request to validate the password - needed to trigger the update of password in Keycloak database.
#Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel)) return false;
UserCredentialModel cred = (UserCredentialModel)input;
// sending a POST request
Response response = userService.validateLogin(user.getUsername(), new EventivalUserCredentialsDto(cred.getValue()));
boolean isValid = HttpStatus.SC_OK == response.getStatus();
if (isValid) {
// save the password to local (keycloak's native) database
session.userCredentialManager().updateCredential(realm, user, cred);
// unset the federation link to never ask again - Import Implementation Strategy
user.setFederationLink(null);
}
return isValid;
}

Log the logins to the various applications that identityserver manages

We've got a lot of sites with common authentication by thinktecture identityserver v2.
Now we would like to have a log of the logins to the sites. We've got a custom IUserRepository where we could log a user login in, but how would we goahead and grab the site a user is loggin into?
And when we jump from one site to another - how could that be logged
In case there's no built in support for this, where is the best place to modify the code?
It seems like it could be done in the WSFederationController and in the Issue method I could get the realm based on the uri.
public ActionResult Issue()
{
Tracing.Start("WS-Federation endpoint.");
if (!ConfigurationRepository.WSFederation.Enabled && ConfigurationRepository.WSFederation.EnableAuthentication)
{
return new HttpNotFoundResult();
}
var message = WSFederationMessage.CreateFromUri(HttpContext.Request.Url);
// sign in
var signinMessage = message as SignInRequestMessage;
if (signinMessage != null)
{
// Is this a good place to log current user and the application the user is loggin into to db???
// or does Thinktecture have some build in functionaltiy for this?
return ProcessWSFederationSignIn(signinMessage, ClaimsPrincipal.Current);
}
Larsi