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.RequestValidators; public interface IDeviceValidator { /// /// Save a device to the database. If the device is already known, it will be returned. /// /// The user is assumed NOT null, still going to check though /// Duende Validated Request that contains the data to create the device object /// Returns null if user or device is malformed; The existing device if already in DB; a new device login Task SaveDeviceAsync(User user, ValidatedTokenRequest request); /// /// Check if a device is known to the user. /// /// current user trying to authenticate /// contains raw information that is parsed about the device /// true if the device is known, false if it is not Task 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; /// /// Save a device to the database. If the device is already known, it will be returned. /// /// The user is assumed NOT null, still going to check though /// Duende Validated Request that contains the data to create the device object /// Returns null if user or device is malformed; The existing device if already in DB; a new device login public async Task 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()?.GetName(); if (!_globalSettings.DisableEmailNewDevice) { await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now, _currentContext.IpAddress); } } return device; } return existingDevice; } return null; } public async Task KnownDeviceAsync(User user, ValidatedTokenRequest request) => (await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default; private async Task 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 }; } }