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

[SG-167] Implement Passwordless Authentication via Notifications (#2276)

* [SG-549] Commit Initial AuthRequest Repository (#2174)

* Model Passwordless

* Scaffold database for Passwordless

* Implement SQL Repository

* [SG-167] Base Passwordless API (#2185)

* Implement Passwordless notifications

* Implement Controller

* Add documentation to BaseRequestValidator

* Register AuthRequestRepo

* Remove ExpirationDate from the AuthRequest table

* [SG-407] Create job to delete expired requests (#2187)

* chore: init

* remove exp date

* fix: log name

* [SG-167] Added fingerprint phrase to response model. (#2233)

* Remove FailedLoginAttempt logic

* Block unknown devices

* Add EF Support for passwordless

* Got SignalR working for responses

* Added delete job method to EF repo

* Implement a GetMany API endpoint for AuthRequests

* Ran dotnet format

* Fix a merge issues

* Redated migration scripts

* tried sorting sqlproj

* Remove FailedLoginAttempts from SQL

* Groom Postgres script

* Remove extra commas from migration script

* Correct isSpent()

* [SG-167] Adde identity validation for passwordless requests. Registered IAuthRepository.

* [SG-167] Added origin of the request to response model

* Use display name for device identifier in response

* Add datetime conversions back to postgres migration script

* [SG-655] Add anonymous endpoint for checking if a device & user combo match

* [review] Consolidate error conditions

Co-authored-by: Brandon Maharaj <107377945+BrandonM-Bitwarden@users.noreply.github.com>
Co-authored-by: André Filipe da Silva Bispo <andrefsbispo@hotmail.com>
Co-authored-by: André Bispo <abispo@bitwarden.com>
This commit is contained in:
Addison Beck
2022-09-26 13:21:13 -04:00
committed by GitHub
parent 7c3637c8ba
commit 02bea3c48d
56 changed files with 5853 additions and 61 deletions

View File

@ -0,0 +1,41 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
namespace Bit.Core.Entities;
public class AuthRequest : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid UserId { get; set; }
public Enums.AuthRequestType Type { get; set; }
[MaxLength(50)]
public string RequestDeviceIdentifier { get; set; }
public Enums.DeviceType RequestDeviceType { get; set; }
[MaxLength(50)]
public string RequestIpAddress { get; set; }
public string RequestFingerprint { get; set; }
public Guid? ResponseDeviceId { get; set; }
[MaxLength(25)]
public string AccessCode { get; set; }
public string PublicKey { get; set; }
public string Key { get; set; }
public string MasterPasswordHash { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime? ResponseDate { get; set; }
public DateTime? AuthenticationDate { get; set; }
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
public bool IsSpent()
{
return ResponseDate.HasValue || AuthenticationDate.HasValue || GetExpirationDate() < DateTime.UtcNow;
}
public DateTime GetExpirationDate()
{
return CreationDate.AddMinutes(15);
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums;
public enum AuthRequestType : byte
{
AuthenticateAndUnlock = 0,
Unlock = 1
}

View File

@ -20,4 +20,7 @@ public enum PushType : byte
SyncSendCreate = 12,
SyncSendUpdate = 13,
SyncSendDelete = 14,
AuthRequest = 15,
AuthRequestResponse = 16,
}

View File

@ -20,6 +20,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
private readonly IUserService _userService;
private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IAuthRequestRepository _authRequestRepository;
public ResourceOwnerPasswordValidator(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
@ -36,6 +37,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
GlobalSettings globalSettings,
IPolicyRepository policyRepository,
ICaptchaValidationService captchaValidationService,
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository)
: base(userManager, deviceRepository, deviceService, userService, eventService,
organizationDuoWebTokenProvider, organizationRepository, organizationUserRepository,
@ -46,6 +48,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
_userService = userService;
_currentContext = currentContext;
_captchaValidationService = captchaValidationService;
_authRequestRepository = authRequestRepository;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
@ -104,12 +107,31 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
return false;
}
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
var authRequestId = context.Request.Raw["AuthRequest"]?.ToString()?.ToLowerInvariant();
if (!string.IsNullOrWhiteSpace(authRequestId) && Guid.TryParse(authRequestId, out var authRequestGuid))
{
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestGuid);
if (authRequest != null)
{
var requestAge = DateTime.UtcNow - authRequest.CreationDate;
if (requestAge < TimeSpan.FromHours(1) && !authRequest.AuthenticationDate.HasValue &&
CoreHelpers.FixedTimeEquals(authRequest.AccessCode, context.Password))
{
authRequest.AuthenticationDate = DateTime.UtcNow;
await _authRequestRepository.ReplaceAsync(authRequest);
return true;
}
}
return false;
}
return true;
else
{
if (!await _userService.CheckPasswordAsync(validatorContext.User, context.Password))
{
return false;
}
return true;
}
}
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,

View File

@ -44,3 +44,9 @@ public class SyncSendPushNotification
public Guid UserId { get; set; }
public DateTime RevisionDate { get; set; }
}
public class AuthRequestPushNotification
{
public Guid UserId { get; set; }
public Guid Id { get; set; }
}

View File

@ -0,0 +1,9 @@
using Bit.Core.Entities;
namespace Bit.Core.Repositories;
public interface IAuthRequestRepository : IRepository<AuthRequest, Guid>
{
Task<int> DeleteExpiredAsync();
Task<ICollection<AuthRequest>> GetManyByUserIdAsync(Guid userId);
}

View File

@ -19,6 +19,8 @@ public interface IPushNotificationService
Task PushSyncSendCreateAsync(Send send);
Task PushSyncSendUpdateAsync(Send send);
Task PushSyncSendDeleteAsync(Send send);
Task PushAuthRequestAsync(AuthRequest authRequest);
Task PushAuthRequestResponseAsync(AuthRequest authRequest);
Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier, string deviceId = null);
Task SendPayloadToOrganizationAsync(string orgId, PushType type, object payload, string identifier,
string deviceId = null);

View File

@ -130,6 +130,27 @@ public class AzureQueuePushNotificationService : IPushNotificationService
await SendMessageAsync(type, message, false);
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
await SendMessageAsync(type, message, true);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);

View File

@ -133,6 +133,18 @@ public class MultiServicePushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
public Task PushAuthRequestAsync(AuthRequest authRequest)
{
PushToServices((s) => s.PushAuthRequestAsync(authRequest));
return Task.FromResult(0);
}
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
PushToServices((s) => s.PushAuthRequestResponseAsync(authRequest));
return Task.FromResult(0);
}
public Task PushSyncSendDeleteAsync(Send send)
{
PushToServices((s) => s.PushSyncSendDeleteAsync(send));

View File

@ -167,6 +167,27 @@ public class NotificationHubPushNotificationService : IPushNotificationService
}
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
{
await SendPayloadToUserAsync(userId.ToString(), type, payload, GetContextIdentifier(excludeCurrentContext));

View File

@ -137,6 +137,27 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
await SendMessageAsync(type, message, false);
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
await SendMessageAsync(type, message, true);
}
public async Task PushSyncSendCreateAsync(Send send)
{
await PushSendAsync(send, PushType.SyncSendCreate);

View File

@ -167,6 +167,27 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
}
}
public async Task PushAuthRequestAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequest);
}
public async Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
await PushAuthRequestAsync(authRequest, PushType.AuthRequestResponse);
}
private async Task PushAuthRequestAsync(AuthRequest authRequest, PushType type)
{
var message = new AuthRequestPushNotification
{
Id = authRequest.Id,
UserId = authRequest.UserId
};
await SendPayloadToUserAsync(authRequest.UserId, type, message, true);
}
private async Task SendPayloadToUserAsync(Guid userId, PushType type, object payload, bool excludeCurrentContext)
{
var request = new PushSendRequestModel

View File

@ -81,6 +81,16 @@ public class NoopPushNotificationService : IPushNotificationService
return Task.FromResult(0);
}
public Task PushAuthRequestAsync(AuthRequest authRequest)
{
return Task.FromResult(0);
}
public Task PushAuthRequestResponseAsync(AuthRequest authRequest)
{
return Task.FromResult(0);
}
public Task SendPayloadToUserAsync(string userId, PushType type, object payload, string identifier,
string deviceId = null)
{

View File

@ -71,6 +71,7 @@ public class GlobalSettings : IGlobalSettings
public virtual ITwoFactorAuthSettings TwoFactorAuth { get; set; } = new TwoFactorAuthSettings();
public virtual DistributedIpRateLimitingSettings DistributedIpRateLimiting { get; set; } =
new DistributedIpRateLimitingSettings();
public virtual IPasswordlessAuthSettings PasswordlessAuth { get; set; } = new PasswordlessAuthSettings();
public string BuildExternalUri(string explicitValue, string name)
{
@ -453,6 +454,7 @@ public class GlobalSettings : IGlobalSettings
get => string.IsNullOrWhiteSpace(_apiUri) ? "https://api.bitwarden.com" : _apiUri;
set => _apiUri = value;
}
}
public class AmazonSettings
@ -519,4 +521,8 @@ public class GlobalSettings : IGlobalSettings
public int SlidingWindowSeconds { get; set; } = 120;
}
public class PasswordlessAuthSettings : IPasswordlessAuthSettings
{
public bool KnownDevicesOnly { get; set; } = true;
}
}

View File

@ -15,4 +15,5 @@ public interface IGlobalSettings
IBaseServiceUriSettings BaseServiceUri { get; set; }
ITwoFactorAuthSettings TwoFactorAuth { get; set; }
ISsoSettings Sso { get; set; }
IPasswordlessAuthSettings PasswordlessAuth { get; set; }
}

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Settings;
public interface IPasswordlessAuthSettings
{
bool KnownDevicesOnly { get; set; }
}