1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-01 16:50:36 -05:00

fix(identity): [PM-21975] Add Security Stamp claim to persisted grant

* Added Security Stamp claim to refresh_token

* Linting

* Added better comments.

* Added clarification to naming of new method.

* Updated comments.

* Added more comments.

* Misspelling
This commit is contained in:
Todd Martin 2025-05-28 16:44:18 -04:00 committed by GitHub
parent 9ad2d61303
commit fe6181f55f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 83 additions and 33 deletions

View File

@ -712,6 +712,7 @@ public static class CoreHelpers
new(Claims.Premium, isPremium ? "true" : "false"),
new(JwtClaimTypes.Email, user.Email),
new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"),
// TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant
new(Claims.SecurityStamp, user.SecurityStamp),
};

View File

@ -72,6 +72,10 @@ public class ProfileService : IProfileService
public async Task IsActiveAsync(IsActiveContext context)
{
// We add the security stamp claim to the persisted grant when we issue the refresh token.
// IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that
// was persisted matches the current security stamp of the user. If it does not match, then the user has performed
// an operation that we want to invalidate the refresh token.
var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp);
var user = await _userService.GetUserByPrincipalAsync(context.Subject);

View File

@ -199,46 +199,26 @@ public abstract class BaseRequestValidator<T> where T : class
protected abstract Task<bool> ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext);
/// <summary>
/// Responsible for building the response to the client when the user has successfully authenticated.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
/// <param name="sendRememberToken">Whether to send a 2FA remember token.</param>
protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken)
{
await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn);
var claims = new List<Claim>();
var claims = this.BuildSubjectClaims(user, context, device);
if (device != null)
{
claims.Add(new Claim(Claims.Device, device.Identifier));
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
}
var customResponse = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
customResponse.Add("PrivateKey", user.PrivateKey);
}
if (!string.IsNullOrWhiteSpace(user.Key))
{
customResponse.Add("Key", user.Key);
}
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);
customResponse.Add("KdfParallelism", user.KdfParallelism);
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
if (sendRememberToken)
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
}
var customResponse = await BuildCustomResponse(user, context, device, sendRememberToken);
await ResetFailedAuthDetailsAsync(user);
// Once we've built the claims and custom response, we can set the success result.
// We delegate this to the derived classes, as the implementation varies based on the grant type.
await SetSuccessResult(context, user, claims, customResponse);
}
@ -392,6 +372,71 @@ public abstract class BaseRequestValidator<T> where T : class
return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user));
}
/// <summary>
/// Builds the claims that will be stored on the persisted grant.
/// These claims are supplemented by the claims in the ProfileService when the access token is returned to the client.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
private List<Claim> BuildSubjectClaims(User user, T context, Device device)
{
// We are adding the security stamp claim to the list of claims that will be stored in the persisted grant.
// We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests,
// in the `ProfileService.IsActiveAsync` method.
// If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against.
var claims = new List<Claim>
{
new Claim(Claims.SecurityStamp, user.SecurityStamp)
};
if (device != null)
{
claims.Add(new Claim(Claims.Device, device.Identifier));
claims.Add(new Claim(Claims.DeviceType, device.Type.ToString()));
}
return claims;
}
/// <summary>
/// Builds the custom response that will be sent to the client upon successful authentication, which
/// includes the information needed for the client to initialize the user's account in state.
/// </summary>
/// <param name="user">The authenticated user.</param>
/// <param name="context">The current request context.</param>
/// <param name="device">The device used for authentication.</param>
/// <param name="sendRememberToken">Whether to send a 2FA remember token.</param>
private async Task<Dictionary<string, object>> BuildCustomResponse(User user, T context, Device device, bool sendRememberToken)
{
var customResponse = new Dictionary<string, object>();
if (!string.IsNullOrWhiteSpace(user.PrivateKey))
{
customResponse.Add("PrivateKey", user.PrivateKey);
}
if (!string.IsNullOrWhiteSpace(user.Key))
{
customResponse.Add("Key", user.Key);
}
customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user));
customResponse.Add("ForcePasswordReset", user.ForcePasswordReset);
customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword));
customResponse.Add("Kdf", (byte)user.Kdf);
customResponse.Add("KdfIterations", user.KdfIterations);
customResponse.Add("KdfMemory", user.KdfMemory);
customResponse.Add("KdfParallelism", user.KdfParallelism);
customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context)));
if (sendRememberToken)
{
var token = await _userManager.GenerateTwoFactorTokenAsync(user,
CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember));
customResponse.Add("TwoFactorToken", token);
}
return customResponse;
}
#nullable enable
/// <summary>
/// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents