Search code examples
asp.net-coreidentityserver4

IdentityServer4 Revoke all reference tokens for a client, except the current one


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!


Solution

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