Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-1338] AuthRequest Event Logs #3046

Merged
13 changes: 13 additions & 0 deletions src/Api/Auth/Controllers/AuthRequestsController.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
Expand Down Expand Up @@ -72,6 +73,18 @@ public async Task<AuthRequestResponseModel> GetResponse(Guid id, [FromQuery] str
[HttpPost("")]
[AllowAnonymous]
public async Task<AuthRequestResponseModel> Post([FromBody] AuthRequestCreateRequestModel model)
{
if (model.Type == AuthRequestType.AdminApproval)
{
throw new BadRequestException("You must be authenticated to create a request of that type.");
}
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
return r;
}

[HttpPost("admin-request")]
public async Task<AuthRequestResponseModel> PostAdminRequest([FromBody] AuthRequestCreateRequestModel model)
{
var authRequest = await _authRequestService.CreateAuthRequestAsync(model);
var r = new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
Expand Down
151 changes: 134 additions & 17 deletions src/Core/Auth/Services/Implementations/AuthRequestService.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using Bit.Core.Auth.Entities;
using System.Diagnostics;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Exceptions;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
Expand All @@ -21,21 +24,27 @@ public class AuthRequestService : IAuthRequestService
private readonly IDeviceRepository _deviceRepository;
private readonly ICurrentContext _currentContext;
private readonly IPushNotificationService _pushNotificationService;
private readonly IEventService _eventService;
private readonly IOrganizationUserRepository _organizationUserRepository;

public AuthRequestService(
IAuthRequestRepository authRequestRepository,
IUserRepository userRepository,
IGlobalSettings globalSettings,
IDeviceRepository deviceRepository,
ICurrentContext currentContext,
IPushNotificationService pushNotificationService)
IPushNotificationService pushNotificationService,
IEventService eventService,
IOrganizationUserRepository organizationRepository)
{
_authRequestRepository = authRequestRepository;
_userRepository = userRepository;
_globalSettings = globalSettings;
_deviceRepository = deviceRepository;
_currentContext = currentContext;
_pushNotificationService = pushNotificationService;
_eventService = eventService;
_organizationUserRepository = organizationRepository;
}

public async Task<AuthRequest?> GetAuthRequestAsync(Guid id, Guid userId)
Expand All @@ -52,9 +61,12 @@ public AuthRequestService(
public async Task<AuthRequest?> GetValidatedAuthRequestAsync(Guid id, string code)
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
{
var authRequest = await _authRequestRepository.GetByIdAsync(id);
if (authRequest == null ||
!CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code) ||
authRequest.GetExpirationDate() < DateTime.UtcNow)
if (authRequest == null || !CoreHelpers.FixedTimeEquals(authRequest.AccessCode, code))
{
return null;
}

if (!IsAuthRequestValid(authRequest))
{
return null;
}
Expand Down Expand Up @@ -91,6 +103,42 @@ public async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestMo
}
}

// AdminApproval requests require correlating the user and their organization
if (model.Type == AuthRequestType.AdminApproval)
{
// TODO: When single org policy is turned on we should query for only a single organization from the current user
// and create only an AuthRequest for that organization and return only that one

// This will send out the request to all organizations this user belongs to
var organizationUsers = await _organizationUserRepository.GetManyByUserAsync(_currentContext.UserId!.Value);

if (organizationUsers.Count == 0)
{
throw new BadRequestException("User does not belong to any organizations.");
}

// A user event will automatically create logs for each organization/provider this user belongs to.
await _eventService.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);

AuthRequest? firstAuthRequest = null;
foreach (var organizationUser in organizationUsers)
{
var createdAuthRequest = await CreateAuthRequestAsync(model, user, organizationUser.OrganizationId);
JaredSnider-Bitwarden marked this conversation as resolved.
Show resolved Hide resolved
firstAuthRequest ??= createdAuthRequest;
}

// I know this won't be null because I have already validated that at least one organization exists
return firstAuthRequest!;
}

var authRequest = await CreateAuthRequestAsync(model, user, organizationId: null);
await _pushNotificationService.PushAuthRequestAsync(authRequest);
return authRequest;
}

private async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestModel model, User user, Guid? organizationId)
{
Debug.Assert(_currentContext.DeviceType.HasValue, "DeviceType should have already been validated to have a value.");
var authRequest = new AuthRequest
{
RequestDeviceIdentifier = model.DeviceIdentifier,
Expand All @@ -100,35 +148,58 @@ public async Task<AuthRequest> CreateAuthRequestAsync(AuthRequestCreateRequestMo
PublicKey = model.PublicKey,
UserId = user.Id,
Type = model.Type.GetValueOrDefault(),
OrganizationId = organizationId,
};

authRequest = await _authRequestRepository.CreateAsync(authRequest);
await _pushNotificationService.PushAuthRequestAsync(authRequest);
return authRequest;
}

public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid userId, AuthRequestUpdateRequestModel model)
public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid currentUserId, AuthRequestUpdateRequestModel model)
{
var authRequest = await _authRequestRepository.GetByIdAsync(authRequestId);
if (authRequest == null || authRequest.UserId != userId || authRequest.GetExpirationDate() < DateTime.UtcNow)

if (authRequest == null)
{
throw new NotFoundException();
}

// Once Approval/Disapproval has been set, this AuthRequest should not be updated again.
if (authRequest.Approved is not null)
{
throw new DuplicateAuthRequestException();
}

// Admin approval responses are not tied to a specific device, so we don't need to validate it.
if (authRequest.Type != AuthRequestType.AdminApproval)
// Do type specific validation
switch (authRequest.Type)
{
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, userId);
if (device == null)
{
throw new BadRequestException("Invalid device.");
}
authRequest.ResponseDeviceId = device.Id;
case AuthRequestType.AdminApproval:
// AdminApproval has a different expiration time, by default is 7 days compared to
// non-AdminApproval ones having a default of 15 minutes.
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration))
{
throw new NotFoundException();
}
break;
case AuthRequestType.AuthenticateAndUnlock:
case AuthRequestType.Unlock:
if (IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration))
{
throw new NotFoundException();
}

if (authRequest.UserId != currentUserId)
{
throw new NotFoundException();
}

// Admin approval responses are not tied to a specific device, but these types are so we need to validate them
var device = await _deviceRepository.GetByIdentifierAsync(model.DeviceIdentifier, currentUserId);
if (device == null)
{
throw new BadRequestException("Invalid device.");
}
authRequest.ResponseDeviceId = device.Id;
break;
}

authRequest.ResponseDate = DateTime.UtcNow;
Expand All @@ -146,9 +217,55 @@ public async Task<AuthRequest> UpdateAuthRequestAsync(Guid authRequestId, Guid u
// to not leak that it was denied to the originating client if it was originated by a malicious actor.
if (authRequest.Approved ?? true)
{
if (authRequest.OrganizationId.HasValue)
{
var organizationUser = await _organizationUserRepository
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_ApprovedAuthRequest);
}

// No matter what we want to push out the success notification
await _pushNotificationService.PushAuthRequestResponseAsync(authRequest);
}
// If the request is rejected by an organization admin then we want to log an event of that action
else if (authRequest.Approved.HasValue && !authRequest.Approved.Value && authRequest.OrganizationId.HasValue)
{
var organizationUser = await _organizationUserRepository
.GetByOrganizationAsync(authRequest.OrganizationId.Value, authRequest.UserId);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_RejectedAuthRequest);
}

return authRequest;
}

private bool IsAuthRequestValid(AuthRequest authRequest)
{
return authRequest.Type switch
{
AuthRequestType.AuthenticateAndUnlock or AuthRequestType.Unlock
=> !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.UserRequestExpiration),
AuthRequestType.AdminApproval => IsAdminApprovalAuthRequestValid(authRequest),
_ => false,
};
}

private bool IsAdminApprovalAuthRequestValid(AuthRequest authRequest)
{
Debug.Assert(authRequest.Type == AuthRequestType.AdminApproval, "This method should only be called on AdminApproval type");
// If an AdminApproval type has been approved it's expiration time is based on how long it's been since approved.
if (authRequest.Approved is true)
{
Debug.Assert(authRequest.ResponseDate.HasValue, "The response date should have been set when the request was updated.");
return !IsDateExpired(authRequest.ResponseDate.Value, _globalSettings.PasswordlessAuth.AfterAdminApprovalExpiration);
}
else
{
return !IsDateExpired(authRequest.CreationDate, _globalSettings.PasswordlessAuth.AdminRequestExpiration);
}
}

private static bool IsDateExpired(DateTime savedDate, TimeSpan allowedLifetime)
{
return DateTime.UtcNow > savedDate.Add(allowedLifetime);
}
}
3 changes: 3 additions & 0 deletions src/Core/Enums/EventType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public enum EventType : int
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
User_RequestedDeviceApproval = 1010,

Cipher_Created = 1100,
Cipher_Updated = 1101,
Expand Down Expand Up @@ -54,6 +55,8 @@ public enum EventType : int
OrganizationUser_FirstSsoLogin = 1510,
OrganizationUser_Revoked = 1511,
OrganizationUser_Restored = 1512,
OrganizationUser_ApprovedAuthRequest = 1513,
OrganizationUser_RejectedAuthRequest = 1514,

Organization_Updated = 1600,
Organization_PurgedVault = 1601,
Expand Down
14 changes: 9 additions & 5 deletions test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using System.Reflection;
#nullable enable

using System.Reflection;
using AutoFixture;
using Bit.Test.Common.Helpers;
using Xunit.Sdk;
Expand All @@ -9,19 +11,21 @@ namespace Bit.Test.Common.AutoFixture.Attributes;
public class BitAutoDataAttribute : DataAttribute
{
private readonly Func<IFixture> _createFixture;
private readonly object[] _fixedTestParameters;
private readonly object?[] _fixedTestParameters;

public BitAutoDataAttribute() : this(Array.Empty<object>()) { }

public BitAutoDataAttribute(params object[] fixedTestParameters) :
public BitAutoDataAttribute(params object?[] fixedTestParameters) :
this(() => new Fixture(), fixedTestParameters)
{ }

public BitAutoDataAttribute(Func<IFixture> createFixture, params object[] fixedTestParameters) :
public BitAutoDataAttribute(Func<IFixture> createFixture, params object?[] fixedTestParameters) :
base()
{
_createFixture = createFixture;
_fixedTestParameters = fixedTestParameters;
}

public override IEnumerable<object[]> GetData(MethodInfo testMethod)
public override IEnumerable<object?[]> GetData(MethodInfo testMethod)
=> BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), _fixedTestParameters);
}
Loading