1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05:00

[PM-15614] Allow Users to opt out of new device verification (#5176)

feat(NewDeviceVerification) : 
* Created database migration scripts for VerifyDevices column in [dbo].[User].
* Updated DeviceValidator to check if user has opted out of device verification.
* Added endpoint to AccountsController.cs to allow editing of new User.VerifyDevices property.
* Added tests for new methods and endpoint.
* Updating queries to track [dbo].[User].[VerifyDevices].
* Updated DeviceValidator to set `User.EmailVerified` property during the New Device Verification flow.
This commit is contained in:
Ike
2025-01-08 07:31:24 -08:00
committed by GitHub
parent 481a766cd2
commit a84ef0724c
21 changed files with 9459 additions and 9 deletions

View File

@ -969,11 +969,28 @@ public class AccountsController : Controller
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[AllowAnonymous]
[HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request)
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
{
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
}
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[HttpPost("verify-devices")]
[HttpPut("verify-devices")]
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
{
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
if (!await _userService.VerifySecretAsync(user, request.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
user.VerifyDevices = request.VerifyDevices;
await _userService.SaveUserAsync(user);
}
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class SetVerifyDevicesRequestModel : SecretVerificationRequestModel
{
[Required]
public bool VerifyDevices { get; set; }
}

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel
public class UnauthenticatedSecretVerificationRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]

View File

@ -72,6 +72,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public DateTime? LastKdfChangeDate { get; set; }
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
public void SetNewId()
{

View File

@ -115,7 +115,7 @@ public class DeviceValidator(
/// </summary>
/// <param name="user">user attempting to authenticate</param>
/// <param name="ValidatedRequest">The Request is used to check for the NewDeviceOtp and for the raw device data</param>
/// <returns>returns deviceValtaionResultType</returns>
/// <returns>returns deviceValidationResultType</returns>
private async Task<DeviceValidationResultType> HandleNewDeviceVerificationAsync(User user, ValidatedRequest request)
{
// currently unreachable due to backward compatibility
@ -125,6 +125,12 @@ public class DeviceValidator(
return DeviceValidationResultType.InvalidUser;
}
// Has the User opted out of new device verification
if (!user.VerifyDevices)
{
return DeviceValidationResultType.Success;
}
// CS exception flow
// Check cache for user information
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, user.Id.ToString());
@ -146,6 +152,12 @@ public class DeviceValidator(
var otpValid = await _userService.VerifyOTPAsync(user, newDeviceOtp);
if (otpValid)
{
// In order to get here they would have to have access to their email so we verify it if it's not already
if (!user.EmailVerified)
{
user.EmailVerified = true;
await _userService.SaveUserAsync(user);
}
return DeviceValidationResultType.Success;
}
return DeviceValidationResultType.InvalidNewDeviceOtp;

View File

@ -40,7 +40,8 @@
@LastPasswordChangeDate DATETIME2(7) = NULL,
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL
@LastEmailChangeDate DATETIME2(7) = NULL,
@VerifyDevices BIT = 1
AS
BEGIN
SET NOCOUNT ON
@ -88,7 +89,8 @@ BEGIN
[LastPasswordChangeDate],
[LastKdfChangeDate],
[LastKeyRotationDate],
[LastEmailChangeDate]
[LastEmailChangeDate],
[VerifyDevices]
)
VALUES
(
@ -133,6 +135,7 @@ BEGIN
@LastPasswordChangeDate,
@LastKdfChangeDate,
@LastKeyRotationDate,
@LastEmailChangeDate
@LastEmailChangeDate,
@VerifyDevices
)
END

View File

@ -40,7 +40,8 @@
@LastPasswordChangeDate DATETIME2(7) = NULL,
@LastKdfChangeDate DATETIME2(7) = NULL,
@LastKeyRotationDate DATETIME2(7) = NULL,
@LastEmailChangeDate DATETIME2(7) = NULL
@LastEmailChangeDate DATETIME2(7) = NULL,
@VerifyDevices BIT = 1
AS
BEGIN
SET NOCOUNT ON
@ -88,7 +89,8 @@ BEGIN
[LastPasswordChangeDate] = @LastPasswordChangeDate,
[LastKdfChangeDate] = @LastKdfChangeDate,
[LastKeyRotationDate] = @LastKeyRotationDate,
[LastEmailChangeDate] = @LastEmailChangeDate
[LastEmailChangeDate] = @LastEmailChangeDate,
[VerifyDevices] = @VerifyDevices
WHERE
[Id] = @Id
END

View File

@ -36,11 +36,12 @@
[UsesKeyConnector] BIT NOT NULL,
[FailedLoginCount] INT CONSTRAINT [D_User_FailedLoginCount] DEFAULT ((0)) NOT NULL,
[LastFailedLoginDate] DATETIME2 (7) NULL,
[AvatarColor] VARCHAR(7) NULL,
[AvatarColor] VARCHAR(7) NULL,
[LastPasswordChangeDate] DATETIME2 (7) NULL,
[LastKdfChangeDate] DATETIME2 (7) NULL,
[LastKeyRotationDate] DATETIME2 (7) NULL,
[LastEmailChangeDate] DATETIME2 (7) NULL,
[VerifyDevices] BIT DEFAULT ((1)) NOT NULL,
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED ([Id] ASC)
);