mirror of
https://github.com/bitwarden/server.git
synced 2025-07-01 08:02:49 -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:
145
src/Api/Controllers/AuthRequestsController.cs
Normal file
145
src/Api/Controllers/AuthRequestsController.cs
Normal file
@ -0,0 +1,145 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers;
|
||||
|
||||
[Route("auth-requests")]
|
||||
[Authorize("Application")]
|
||||
public class AuthRequestsController : Controller
|
||||
{
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IPushNotificationService _pushNotificationService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public AuthRequestsController(
|
||||
IUserRepository userRepository,
|
||||
IDeviceRepository deviceRepository,
|
||||
IUserService userService,
|
||||
IAuthRequestRepository authRequestRepository,
|
||||
ICurrentContext currentContext,
|
||||
IPushNotificationService pushNotificationService,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_deviceRepository = deviceRepository;
|
||||
_userService = userService;
|
||||
_authRequestRepository = authRequestRepository;
|
||||
_currentContext = currentContext;
|
||||
_pushNotificationService = pushNotificationService;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> Get()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);
|
||||
var responses = authRequests.Select(a => new AuthRequestResponseModel(a, _globalSettings.SelfHosted)).ToList();
|
||||
return new ListResponseModel<AuthRequestResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
public async Task<AuthRequestResponseModel> Get(string id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || authRequest.UserId != userId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.SelfHosted);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/response")]
|
||||
[AllowAnonymous]
|
||||
public async Task<AuthRequestResponseModel> GetResponse(string id, [FromQuery] string code)
|
||||
{
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || code != authRequest.AccessCode || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.SelfHosted);
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
[AllowAnonymous]
|
||||
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
|
||||
{
|
||||
var user = await _userRepository.GetByEmailAsync(model.Email);
|
||||
if (user == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!_currentContext.DeviceType.HasValue)
|
||||
{
|
||||
throw new BadRequestException("Device type not provided.");
|
||||
}
|
||||
if (!_globalSettings.PasswordlessAuth.KnownDevicesOnly)
|
||||
{
|
||||
var d = await _deviceRepository.GetByIdentifierAsync(_currentContext.DeviceIdentifier);
|
||||
if (d == null || d.UserId != user.Id)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
var authRequest = new AuthRequest
|
||||
{
|
||||
RequestDeviceIdentifier = model.DeviceIdentifier,
|
||||
RequestDeviceType = _currentContext.DeviceType.Value,
|
||||
RequestIpAddress = _currentContext.IpAddress,
|
||||
AccessCode = model.AccessCode,
|
||||
PublicKey = model.PublicKey,
|
||||
UserId = user.Id,
|
||||
Type = model.Type.Value,
|
||||
RequestFingerprint = model.FingerprintPhrase
|
||||
};
|
||||
authRequest = await _authRequestRepository.CreateAsync(authRequest);
|
||||
await _pushNotificationService.PushAuthRequestAsync(authRequest);
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.SelfHosted);
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
public async Task<AuthRequestResponseModel> Put(string id, [FromBody] AuthRequestUpdateRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(id));
|
||||
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier);
|
||||
if (device == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid device.");
|
||||
}
|
||||
|
||||
if (model.RequestApproved)
|
||||
{
|
||||
authRequest.Key = model.Key;
|
||||
authRequest.MasterPasswordHash = model.MasterPasswordHash;
|
||||
authRequest.ResponseDeviceId = device.Id;
|
||||
authRequest.ResponseDate = DateTime.UtcNow;
|
||||
await _authRequestRepository.ReplaceAsync(authRequest);
|
||||
}
|
||||
|
||||
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
|
||||
return new AuthRequestResponseModel(authRequest, _globalSettings.SelfHosted);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user