mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 16:12:49 -05:00
[PM-10742] Pull Device verification into testable service (#4851)
* initial device removal * Unit Testing * Added unit tests fixed validator null checks * Finalized tests * formatting * fixed test * lint * addressing review notes * comments
This commit is contained in:
@ -1,6 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
@ -33,9 +31,8 @@ namespace Bit.Identity.IdentityServer;
|
||||
public abstract class BaseRequestValidator<T> where T : class
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
@ -56,10 +53,9 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -77,10 +73,9 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_deviceRepository = deviceRepository;
|
||||
_deviceService = deviceService;
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
_deviceValidator = deviceValidator;
|
||||
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
|
||||
_duoWebV4SDKService = duoWebV4SDKService;
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -131,9 +126,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
|
||||
if (isTwoFactorRequired)
|
||||
{
|
||||
// Just defaulting it
|
||||
var twoFactorProviderType = TwoFactorProviderType.Authenticator;
|
||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
|
||||
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
|
||||
{
|
||||
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
|
||||
return;
|
||||
@ -162,7 +155,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
twoFactorToken = null;
|
||||
}
|
||||
|
||||
|
||||
// Force legacy users to the web for migration
|
||||
if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers))
|
||||
{
|
||||
@ -176,7 +168,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
// Returns true if can finish validation process
|
||||
if (await IsValidAuthTypeAsync(user, request.GrantType))
|
||||
{
|
||||
var device = await SaveDeviceAsync(user, request);
|
||||
var device = await _deviceValidator.SaveDeviceAsync(user, request);
|
||||
if (device == null)
|
||||
{
|
||||
await BuildErrorResultAsync("No device information provided.", false, context, user);
|
||||
@ -393,28 +385,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
|
||||
}
|
||||
|
||||
private Device GetDeviceFromRequest(ValidatedRequest request)
|
||||
{
|
||||
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
|
||||
var deviceType = request.Raw["DeviceType"]?.ToString();
|
||||
var deviceName = request.Raw["DeviceName"]?.ToString();
|
||||
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
|
||||
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Device
|
||||
{
|
||||
Identifier = deviceIdentifier,
|
||||
Name = deviceName,
|
||||
Type = type,
|
||||
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
|
||||
string token)
|
||||
{
|
||||
@ -537,51 +507,6 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
|
||||
(await GetKnownDeviceAsync(user, request)) != default;
|
||||
|
||||
protected async Task<Device> GetKnownDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
if (user == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
|
||||
}
|
||||
|
||||
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
||||
if (device != null)
|
||||
{
|
||||
var existingDevice = await GetKnownDeviceAsync(user, request);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
device.UserId = user.Id;
|
||||
await _deviceService.SaveAsync(device);
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
||||
{
|
||||
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
if (!_globalSettings.DisableEmailNewDevice)
|
||||
{
|
||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||
CurrentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return device;
|
||||
}
|
||||
|
||||
return existingDevice;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task ResetFailedAuthDetailsAsync(User user)
|
||||
{
|
||||
// Early escape if db hit not necessary
|
||||
|
@ -29,8 +29,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
|
||||
public CustomTokenRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
@ -48,7 +47,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
|
||||
@ -83,11 +82,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
{
|
||||
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await ValidateAsync(context, context.Result.ValidatedRequest,
|
||||
new CustomValidatorRequestContext { KnownDevice = true });
|
||||
}
|
||||
@ -103,7 +99,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
{
|
||||
validatorContext.User = await _userManager.FindByEmailAsync(email);
|
||||
}
|
||||
|
||||
return validatorContext.User != null;
|
||||
}
|
||||
|
||||
@ -121,7 +116,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
context.Result.ValidatedRequest.ClientClaims.Add(claim);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.Result.CustomResponse == null || user.MasterPassword != null)
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
@ -138,7 +132,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@ -150,13 +143,11 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
{
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (userDecryptionOptions is { KeyConnectorOption: { } })
|
||||
{
|
||||
context.Result.CustomResponse["KeyConnectorUrl"] = userDecryptionOptions.KeyConnectorOption.KeyConnectorUrl;
|
||||
context.Result.CustomResponse["ResetMasterPassword"] = false;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
109
src/Identity/IdentityServer/DeviceValidator.cs
Normal file
109
src/Identity/IdentityServer/DeviceValidator.cs
Normal file
@ -0,0 +1,109 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Reflection;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
public interface IDeviceValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Save a device to the database. If the device is already known, it will be returned.
|
||||
/// </summary>
|
||||
/// <param name="user">The user is assumed NOT null, still going to check though</param>
|
||||
/// <param name="request">Duende Validated Request that contains the data to create the device object</param>
|
||||
/// <returns>Returns null if user or device is malformed; The existing device if already in DB; a new device login</returns>
|
||||
Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request);
|
||||
/// <summary>
|
||||
/// Check if a device is known to the user.
|
||||
/// </summary>
|
||||
/// <param name="user">current user trying to authenticate</param>
|
||||
/// <param name="request">contains raw information that is parsed about the device</param>
|
||||
/// <returns>true if the device is known, false if it is not</returns>
|
||||
Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request);
|
||||
}
|
||||
|
||||
public class DeviceValidator(
|
||||
IDeviceService deviceService,
|
||||
IDeviceRepository deviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IMailService mailService,
|
||||
ICurrentContext currentContext) : IDeviceValidator
|
||||
{
|
||||
private readonly IDeviceService _deviceService = deviceService;
|
||||
private readonly IDeviceRepository _deviceRepository = deviceRepository;
|
||||
private readonly GlobalSettings _globalSettings = globalSettings;
|
||||
private readonly IMailService _mailService = mailService;
|
||||
private readonly ICurrentContext _currentContext = currentContext;
|
||||
|
||||
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
|
||||
{
|
||||
var device = GetDeviceFromRequest(request);
|
||||
if (device != null && user != null)
|
||||
{
|
||||
var existingDevice = await GetKnownDeviceAsync(user, device);
|
||||
if (existingDevice == null)
|
||||
{
|
||||
device.UserId = user.Id;
|
||||
await _deviceService.SaveAsync(device);
|
||||
|
||||
// This makes sure the user isn't sent a "new device" email on their first login
|
||||
var now = DateTime.UtcNow;
|
||||
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
|
||||
{
|
||||
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
|
||||
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
|
||||
if (!_globalSettings.DisableEmailNewDevice)
|
||||
{
|
||||
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
|
||||
_currentContext.IpAddress);
|
||||
}
|
||||
}
|
||||
return device;
|
||||
}
|
||||
return existingDevice;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
|
||||
(await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default;
|
||||
|
||||
private async Task<Device> GetKnownDeviceAsync(User user, Device device)
|
||||
{
|
||||
if (user == null || device == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);
|
||||
}
|
||||
|
||||
private static Device GetDeviceFromRequest(ValidatedRequest request)
|
||||
{
|
||||
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
|
||||
var requestDeviceType = request.Raw["DeviceType"]?.ToString();
|
||||
var deviceName = request.Raw["DeviceName"]?.ToString();
|
||||
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(deviceIdentifier) ||
|
||||
string.IsNullOrWhiteSpace(requestDeviceType) ||
|
||||
string.IsNullOrWhiteSpace(deviceName) ||
|
||||
!Enum.TryParse(requestDeviceType, out DeviceType parsedDeviceType))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Device
|
||||
{
|
||||
Identifier = deviceIdentifier,
|
||||
Name = deviceName,
|
||||
Type = parsedDeviceType,
|
||||
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
|
||||
};
|
||||
}
|
||||
}
|
@ -25,12 +25,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ICaptchaValidationService _captchaValidationService;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
public ResourceOwnerPasswordValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -48,7 +48,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
|
||||
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
@ -57,6 +57,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
_currentContext = currentContext;
|
||||
_captchaValidationService = captchaValidationService;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_deviceValidator = deviceValidator;
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
|
||||
@ -72,7 +73,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
var validatorContext = new CustomValidatorRequestContext
|
||||
{
|
||||
User = user,
|
||||
KnownDevice = await KnownDeviceAsync(user, context.Request)
|
||||
KnownDevice = await _deviceValidator.KnownDeviceAsync(user, context.Request),
|
||||
};
|
||||
string bypassToken = null;
|
||||
if (!validatorContext.KnownDevice &&
|
||||
|
@ -27,13 +27,13 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
|
||||
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
private readonly IDeviceValidator _deviceValidator;
|
||||
|
||||
public WebAuthnGrantValidator(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IDeviceValidator deviceValidator,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
@ -52,13 +52,14 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
||||
)
|
||||
: base(userManager, deviceRepository, deviceService, userService, eventService,
|
||||
: base(userManager, userService, eventService, deviceValidator,
|
||||
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
|
||||
applicationCacheService, mailService, logger, currentContext, globalSettings,
|
||||
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
_deviceValidator = deviceValidator;
|
||||
}
|
||||
|
||||
string IExtensionGrantValidator.GrantType => "webauthn";
|
||||
@ -87,7 +88,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
var validatorContext = new CustomValidatorRequestContext
|
||||
{
|
||||
User = user,
|
||||
KnownDevice = await KnownDeviceAsync(user, context.Request)
|
||||
KnownDevice = await _deviceValidator.KnownDeviceAsync(user, context.Request)
|
||||
};
|
||||
|
||||
UserDecryptionOptionsBuilder.WithWebAuthnLoginCredential(credential);
|
||||
|
@ -20,6 +20,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<StaticClientStore>();
|
||||
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
|
||||
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
|
||||
services.AddTransient<IDeviceValidator, DeviceValidator>();
|
||||
|
||||
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
|
||||
var identityServerBuilder = services
|
||||
|
Reference in New Issue
Block a user