1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-04 20:50:21 -05:00

[PM-19279] Add prelogin response (#5511)

* Add prelogin response

* Fix test

* Fix more tests

* Fix tests

* Fix SQL warnings

* Fix difference between migration and sql SP

* Attempt to fix tests

* Attempt to fix tests

* Attempt to fix

* Fix namespace

* Attempt to fix error

* Fix different SP / migration

* Attempt to fix migration

* Fix

* Fix
This commit is contained in:
Bernd Schoolmann 2025-03-19 11:34:33 +01:00 committed by GitHub
parent 2fd1b25580
commit 7a8ee710da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 239 additions and 24 deletions

View File

@ -15,7 +15,6 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Entities;
@ -58,7 +57,6 @@ public class AccountsController : Controller
_organizationUserValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator;
private readonly IOpaqueKeyExchangeService _opaqueKeyExchangeService;
public AccountsController(
@ -78,8 +76,7 @@ public class AccountsController : Controller
emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
IOpaqueKeyExchangeService opaqueKeyExchangeService
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
)
{
_organizationService = organizationService;
@ -97,7 +94,6 @@ public class AccountsController : Controller
_emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator;
_opaqueKeyExchangeService = opaqueKeyExchangeService;
}

View File

@ -1,10 +1,13 @@
using System.Diagnostics;
using System.Text;
using System.Text.Json;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
@ -45,6 +48,7 @@ public class AccountsController : Controller
private readonly IReferenceEventService _referenceEventService;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly IOpaqueKeyExchangeCredentialRepository _opaqueKeyExchangeCredentialRepository;
private readonly byte[] _defaultKdfHmacKey = null;
private static readonly List<UserKdfInformation> _defaultKdfResults =
@ -93,6 +97,7 @@ public class AccountsController : Controller
IReferenceEventService referenceEventService,
IFeatureService featureService,
IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> registrationEmailVerificationTokenDataFactory,
IOpaqueKeyExchangeCredentialRepository opaqueKeyExchangeCredentialRepository,
GlobalSettings globalSettings
)
{
@ -107,6 +112,7 @@ public class AccountsController : Controller
_referenceEventService = referenceEventService;
_featureService = featureService;
_registrationEmailVerificationTokenDataFactory = registrationEmailVerificationTokenDataFactory;
_opaqueKeyExchangeCredentialRepository = opaqueKeyExchangeCredentialRepository;
if (CoreHelpers.SettingHasValue(globalSettings.KdfDefaultHashKey))
{
@ -255,11 +261,21 @@ public class AccountsController : Controller
public async Task<PreloginResponseModel> PostPrelogin([FromBody] PreloginRequestModel model)
{
var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email);
if (kdfInformation == null)
var user = await _userRepository.GetByEmailAsync(model.Email);
if (kdfInformation == null || user == null)
{
kdfInformation = GetDefaultKdf(model.Email);
}
return new PreloginResponseModel(kdfInformation);
var credential = await _opaqueKeyExchangeCredentialRepository.GetByUserIdAsync(user.Id);
if (credential != null)
{
return new PreloginResponseModel(kdfInformation, JsonSerializer.Deserialize<CipherConfiguration>(credential.CipherConfiguration)!);
}
else
{
return new PreloginResponseModel(kdfInformation, null);
}
}
[HttpGet("webauthn/assertion-options")]

View File

@ -1,20 +1,23 @@
using Bit.Core.Enums;
using Bit.Core.Auth.Models.Api.Request.Opaque;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Identity.Models.Response.Accounts;
public class PreloginResponseModel
{
public PreloginResponseModel(UserKdfInformation kdfInformation)
public PreloginResponseModel(UserKdfInformation kdfInformation, CipherConfiguration opaqueConfiguration)
{
Kdf = kdfInformation.Kdf;
KdfIterations = kdfInformation.KdfIterations;
KdfMemory = kdfInformation.KdfMemory;
KdfParallelism = kdfInformation.KdfParallelism;
OpaqueConfiguration = opaqueConfiguration;
}
public KdfType Kdf { get; set; }
public int KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
public CipherConfiguration OpaqueConfiguration { get; set; }
}

View File

@ -0,0 +1,24 @@
using AutoMapper;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.KeyManagement.UserKey;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Infrastructure.EntityFramework.Repositories;
public class OpaqueKeyExchangeCredentialRepository : Repository<OpaqueKeyExchangeCredential, OpaqueKeyExchangeCredential, Guid>, IOpaqueKeyExchangeCredentialRepository
{
public OpaqueKeyExchangeCredentialRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : base(serviceScopeFactory, mapper, (DatabaseContext context) => null)
{
}
public Task<OpaqueKeyExchangeCredential> GetByUserIdAsync(Guid userId)
{
return null;
}
public UpdateEncryptedDataForKeyRotation UpdateKeysForRotationAsync(Guid userId, IEnumerable<OpaqueKeyExchangeRotateKeyData> credentials)
{
return null;
}
}

View File

@ -87,6 +87,7 @@ public static class EntityFrameworkServiceCollectionExtensions
services.AddSingleton<IProviderUserRepository, ProviderUserRepository>();
services.AddSingleton<ISendRepository, SendRepository>();
services.AddSingleton<ISsoConfigRepository, SsoConfigRepository>();
services.AddSingleton<IOpaqueKeyExchangeCredentialRepository, OpaqueKeyExchangeCredentialRepository>();
services.AddSingleton<ISsoUserRepository, SsoUserRepository>();
services.AddSingleton<ITransactionRepository, TransactionRepository>();
services.AddSingleton<IUserRepository, UserRepository>();

View File

@ -110,6 +110,7 @@ public static class ServiceCollectionExtensions
public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings)
{
services.AddScoped<ICipherService, CipherService>();
services.AddSingleton<IOpaqueKeyExchangeService, OpaqueKeyExchangeService>();
services.AddUserServices(globalSettings);
services.AddTrialInitiationServices();
services.AddOrganizationServices(globalSettings);
@ -118,7 +119,6 @@ public static class ServiceCollectionExtensions
services.AddScoped<IGroupService, GroupService>();
services.AddScoped<IEventService, EventService>();
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddScoped<IOpaqueKeyExchangeService, OpaqueKeyExchangeService>();
services.AddSingleton<IDeviceService, DeviceService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<IAuthRequestService, AuthRequestService>();

View File

@ -1,11 +1,11 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@CipherConfiguration VARCHAR(MAX) NOT NULL,
@CredentialBlob VARCHAR(MAX) NOT NULL,
@EncryptedPublicKey VARCHAR(MAX) NOT NULL,
@EncryptedPrivateKey VARCHAR(MAX) NOT NULL,
@EncryptedUserKey VARCHAR(MAX) NOT NULL,
@CipherConfiguration VARCHAR(MAX),
@CredentialBlob VARCHAR(MAX),
@EncryptedPublicKey VARCHAR(MAX),
@EncryptedPrivateKey VARCHAR(MAX),
@EncryptedUserKey VARCHAR(MAX),
@CreationDate DATETIME2(7)
AS
BEGIN

View File

@ -1,5 +1,5 @@
CREATE PROCEDURE [dbo].[OpaqueKeyExchangeCredential_DeleteById]
@Id UNIQUEIDENTIFIER,
@Id UNIQUEIDENTIFIER
AS
BEGIN
DELETE

View File

@ -29,7 +29,7 @@ BEGIN
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[UserId] = @UserId
[UserId] = @Id
-- Delete WebAuthnCredentials
DELETE

View File

@ -291,12 +291,12 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, default, default, default, default, null)
.Returns(Task.FromResult(IdentityResult.Success));
await _sut.PostPassword(new PasswordRequestModel());
await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default);
await _userService.Received(1).ChangePasswordAsync(user, default, default, default, default, null);
}
[Fact]
@ -314,7 +314,7 @@ public class AccountsControllerTests : IDisposable
{
var user = GenerateExampleUser();
ConfigureUserServiceToReturnValidPrincipalFor(user);
_userService.ChangePasswordAsync(user, default, default, default, default)
_userService.ChangePasswordAsync(user, default, default, default, default, null)
.Returns(Task.FromResult(IdentityResult.Failed()));
await Assert.ThrowsAsync<BadRequestException>(

View File

@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
@ -324,7 +325,8 @@ public class UserServiceTests
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IOpaqueKeyExchangeService>()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
@ -911,7 +913,8 @@ public class UserServiceTests
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>(),
sutProvider.GetDependency<IRevokeNonCompliantOrganizationUserCommand>(),
sutProvider.GetDependency<IDistributedCache>()
sutProvider.GetDependency<IDistributedCache>(),
sutProvider.GetDependency<IOpaqueKeyExchangeService>()
);
}
}

View File

@ -3,6 +3,7 @@ using System.Text;
using Bit.Core;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.Registration;
using Bit.Core.Auth.UserFeatures.WebAuthnLogin;
@ -45,6 +46,7 @@ public class AccountsControllerTests : IDisposable
private readonly IReferenceEventService _referenceEventService;
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable> _registrationEmailVerificationTokenDataFactory;
private readonly IOpaqueKeyExchangeCredentialRepository _opaqueKeyExchangeCredentialRepository;
private readonly GlobalSettings _globalSettings;
@ -61,6 +63,7 @@ public class AccountsControllerTests : IDisposable
_referenceEventService = Substitute.For<IReferenceEventService>();
_featureService = Substitute.For<IFeatureService>();
_registrationEmailVerificationTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<RegistrationEmailVerificationTokenable>>();
_opaqueKeyExchangeCredentialRepository = Substitute.For<IOpaqueKeyExchangeCredentialRepository>();
_globalSettings = Substitute.For<GlobalSettings>();
_sut = new AccountsController(
@ -75,6 +78,7 @@ public class AccountsControllerTests : IDisposable
_referenceEventService,
_featureService,
_registrationEmailVerificationTokenDataFactory,
_opaqueKeyExchangeCredentialRepository,
_globalSettings
);
}
@ -93,6 +97,11 @@ public class AccountsControllerTests : IDisposable
KdfIterations = AuthConstants.PBKDF2_ITERATIONS.Default
};
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(userKdfInfo);
var mockUser = new User
{
Email = "user@example.com"
};
_userRepository.GetByEmailAsync(Arg.Any<string>()).Returns(mockUser);
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
@ -105,6 +114,11 @@ public class AccountsControllerTests : IDisposable
{
SetDefaultKdfHmacKey(null);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
var mockUser = new User
{
Email = "user@example.com"
};
_userRepository.GetByEmailAsync(Arg.Any<string>()).Returns(mockUser);
var response = await _sut.PostPrelogin(new PreloginRequestModel { Email = "user@example.com" });
@ -121,6 +135,11 @@ public class AccountsControllerTests : IDisposable
SetDefaultKdfHmacKey(defaultKey);
_userRepository.GetKdfInformationByEmailAsync(Arg.Any<string>()).Returns(Task.FromResult<UserKdfInformation?>(null));
var mockUser = new User
{
Email = "user@example.com"
};
_userRepository.GetByEmailAsync(Arg.Any<string>()).Returns(mockUser);
var fieldInfo = typeof(AccountsController).GetField("_defaultKdfResults", BindingFlags.NonPublic | BindingFlags.Static);
if (fieldInfo == null)

View File

@ -25,7 +25,7 @@ CREATE OR ALTER PROCEDURE [dbo].[OpaqueKeyExchangeCredential_Create]
@CipherConfiguration VARCHAR(MAX),
@CredentialBlob VARCHAR(MAX),
@EncryptedPublicKey VARCHAR(MAX),
@EncryptedPrivateKey TINYINT,
@EncryptedPrivateKey VARCHAR(MAX),
@EncryptedUserKey VARCHAR(MAX),
@CreationDate DATETIME2(7)
AS
@ -127,4 +127,157 @@ BEGIN
[Id] = @Id
END
GO
GO
ALTER PROCEDURE [dbo].[User_DeleteById]
@Id UNIQUEIDENTIFIER
WITH
RECOMPILE
AS
BEGIN
SET NOCOUNT ON
DECLARE @BatchSize INT = 100
-- Delete ciphers
WHILE @BatchSize > 0
BEGIN
BEGIN TRANSACTION User_DeleteById_Ciphers
DELETE TOP(@BatchSize)
FROM
[dbo].[Cipher]
WHERE
[UserId] = @Id
SET @BatchSize = @@ROWCOUNT
COMMIT TRANSACTION User_DeleteById_Ciphers
END
BEGIN TRANSACTION User_DeleteById
-- Delete OpaqueKeyExchangeCredentials
DELETE
FROM
[dbo].[OpaqueKeyExchangeCredential]
WHERE
[UserId] = @Id
-- Delete WebAuthnCredentials
DELETE
FROM
[dbo].[WebAuthnCredential]
WHERE
[UserId] = @Id
-- Delete folders
DELETE
FROM
[dbo].[Folder]
WHERE
[UserId] = @Id
-- Delete AuthRequest, must be before Device
DELETE
FROM
[dbo].[AuthRequest]
WHERE
[UserId] = @Id
-- Delete devices
DELETE
FROM
[dbo].[Device]
WHERE
[UserId] = @Id
-- Delete collection users
DELETE
CU
FROM
[dbo].[CollectionUser] CU
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]
WHERE
OU.[UserId] = @Id
-- Delete group users
DELETE
GU
FROM
[dbo].[GroupUser] GU
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]
WHERE
OU.[UserId] = @Id
-- Delete AccessPolicy
DELETE
AP
FROM
[dbo].[AccessPolicy] AP
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId]
WHERE
[UserId] = @Id
-- Delete organization users
DELETE
FROM
[dbo].[OrganizationUser]
WHERE
[UserId] = @Id
-- Delete provider users
DELETE
FROM
[dbo].[ProviderUser]
WHERE
[UserId] = @Id
-- Delete SSO Users
DELETE
FROM
[dbo].[SsoUser]
WHERE
[UserId] = @Id
-- Delete Emergency Accesses
DELETE
FROM
[dbo].[EmergencyAccess]
WHERE
[GrantorId] = @Id
OR
[GranteeId] = @Id
-- Delete Sends
DELETE
FROM
[dbo].[Send]
WHERE
[UserId] = @Id
-- Delete Notification Status
DELETE
FROM
[dbo].[NotificationStatus]
WHERE
[UserId] = @Id
-- Delete Notification
DELETE
FROM
[dbo].[Notification]
WHERE
[UserId] = @Id
-- Finally, delete the user
DELETE
FROM
[dbo].[User]
WHERE
[Id] = @Id
COMMIT TRANSACTION User_DeleteById
END
GO