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;
}
}