mirror of
https://github.com/bitwarden/server
synced 2026-02-25 17:03:22 +00:00
[PM-31052][PM-32469] Add V2UpgradeToken for key rotation without logout (#6995)
* User V2UpgradeToken for key rotation without logout * reset old v2 upgrade token on manual key rotation * sql migration fix * missing table column * missing view update * tests for V2UpgradeToken clearing on manual key rotation * V2 to V2 rotation causes logout. Updated wrapped key 1 to be a valid V2 encrypted string in tests. * integration tests failures - increase assert recent for date time type from 2 to 5 seconds (usually for UpdatedAt assertions) * repository test coverage * migration script update * new EF migration scripts * broken EF migration scripts fixed * refresh views due to User table alternation
This commit is contained in:
@@ -15,4 +15,10 @@ public class UserDecryptionResponseModel
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public WebAuthnPrfDecryptionOption[]? WebAuthnPrfOptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// V2 upgrade token returned when available, allowing unlock after V1→V2 upgrade.
|
||||
/// </summary>
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public V2UpgradeTokenResponseModel? V2UpgradeToken { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.KeyManagement.Models.Api.Response;
|
||||
|
||||
public class V2UpgradeTokenResponseModel
|
||||
{
|
||||
public required string WrappedUserKey1 { get; set; }
|
||||
public required string WrappedUserKey2 { get; set; }
|
||||
}
|
||||
@@ -20,6 +20,7 @@ public class RotateUserAccountKeysData
|
||||
public required IReadOnlyList<OrganizationUser> OrganizationUsers { get; set; }
|
||||
public required IEnumerable<WebAuthnLoginRotateKeyData> WebAuthnKeys { get; set; }
|
||||
public required IEnumerable<Device> DeviceKeys { get; set; }
|
||||
public V2UpgradeTokenData? V2UpgradeToken { get; set; }
|
||||
|
||||
// User vault data encrypted by the userkey
|
||||
public required IEnumerable<Cipher> Ciphers { get; set; }
|
||||
|
||||
31
src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs
Normal file
31
src/Core/KeyManagement/Models/Data/V2UpgradeTokenData.cs
Normal file
@@ -0,0 +1,31 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Bit.Core.KeyManagement.Models.Data;
|
||||
|
||||
public class V2UpgradeTokenData
|
||||
{
|
||||
public required string WrappedUserKey1 { get; init; }
|
||||
public required string WrappedUserKey2 { get; init; }
|
||||
|
||||
public string ToJson()
|
||||
{
|
||||
return JsonSerializer.Serialize(this);
|
||||
}
|
||||
|
||||
public static V2UpgradeTokenData? FromJson(string? json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<V2UpgradeTokenData>(json);
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,7 +90,19 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
var now = DateTime.UtcNow;
|
||||
user.RevisionDate = user.AccountRevisionDate = now;
|
||||
user.LastKeyRotationDate = now;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
|
||||
// V2UpgradeToken is only valid for V1 users transitioning to V2.
|
||||
// For V2 users the token is semantically invalid — discard it and perform a full logout.
|
||||
var shouldPersistV2UpgradeToken = model.V2UpgradeToken != null && !IsV2EncryptionUserAsync(user);
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
user.V2UpgradeToken = model.V2UpgradeToken!.ToJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
user.V2UpgradeToken = null;
|
||||
user.SecurityStamp = Guid.NewGuid().ToString();
|
||||
}
|
||||
|
||||
List<UpdateEncryptedDataForKeyRotation> saveEncryptedDataActions = [];
|
||||
|
||||
@@ -99,7 +111,17 @@ public class RotateUserAccountKeysCommand : IRotateUserAccountKeysCommand
|
||||
UpdateUserData(model, user, saveEncryptedDataActions);
|
||||
|
||||
await _userRepository.UpdateUserKeyAndEncryptedDataV2Async(user, saveEncryptedDataActions);
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
|
||||
if (shouldPersistV2UpgradeToken)
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id,
|
||||
reason: PushNotificationLogOutReason.KeyRotation);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _pushService.PushLogOutAsync(user.Id);
|
||||
}
|
||||
|
||||
return IdentityResult.Success;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user