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:
parent
9ad2d61303
commit
fe6181f55f
@ -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),
|
||||
};
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user