Skip to content
17 changes: 9 additions & 8 deletions src/Api/Auth/Controllers/TwoFactorController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Services;
using Bit.Core.Context;
Expand All @@ -35,7 +34,7 @@ public class TwoFactorController : Controller
private readonly IOrganizationService _organizationService;
private readonly UserManager<User> _userManager;
private readonly ICurrentContext _currentContext;
private readonly IVerifyAuthRequestCommand _verifyAuthRequestCommand;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDuoUniversalTokenService _duoUniversalTokenService;
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
Expand All @@ -47,7 +46,7 @@ public TwoFactorController(
IOrganizationService organizationService,
UserManager<User> userManager,
ICurrentContext currentContext,
IVerifyAuthRequestCommand verifyAuthRequestCommand,
IAuthRequestRepository authRequestRepository,
IDuoUniversalTokenService duoUniversalConfigService,
IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> twoFactorAuthenticatorDataProtector,
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> ssoEmailTwoFactorSessionDataProtector,
Expand All @@ -58,7 +57,7 @@ public TwoFactorController(
_organizationService = organizationService;
_userManager = userManager;
_currentContext = currentContext;
_verifyAuthRequestCommand = verifyAuthRequestCommand;
_authRequestRepository = authRequestRepository;
_duoUniversalTokenService = duoUniversalConfigService;
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
Expand Down Expand Up @@ -350,14 +349,16 @@ public async Task SendEmailLoginAsync([FromBody] TwoFactorEmailRequestModel requ

if (user != null)
{
// Check if 2FA email is from Passwordless.
// Check if 2FA email is from a device approval ("Log in with device") scenario.
// 2FA is required for an unknown device.
if (!string.IsNullOrEmpty(requestModel.AuthRequestAccessCode))
{
if (await _verifyAuthRequestCommand
.VerifyAuthRequestAsync(new Guid(requestModel.AuthRequestId),
requestModel.AuthRequestAccessCode))
var authRequest = await _authRequestRepository.GetByIdAsync(new Guid(requestModel.AuthRequestId));
if (authRequest != null &&
authRequest.IsValidForAuthentication(user.Id, requestModel.AuthRequestAccessCode))
{
await _twoFactorEmailService.SendTwoFactorEmailAsync(user);
return;
}
}
else if (!string.IsNullOrEmpty(requestModel.SsoEmail2FaSessionToken))
Expand Down
2 changes: 0 additions & 2 deletions src/Core/Auth/Entities/AuthRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ public bool IsSpent()

public bool IsExpired()
{
// TODO: PM-24252 - consider using TimeProvider for better mocking in tests
return GetExpirationDate() < DateTime.UtcNow;
}

// TODO: PM-24252 - this probably belongs in a service.
public bool IsValidForAuthentication(Guid userId,
string password)
{
Expand Down
14 changes: 0 additions & 14 deletions src/Core/Auth/LoginFeatures/LoginServiceCollectionExtensions.cs

This file was deleted.

This file was deleted.

This file was deleted.

2 changes: 0 additions & 2 deletions src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.IdentityServer;
using Bit.Core.Auth.LoginFeatures;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.Services;
Expand Down Expand Up @@ -140,7 +139,6 @@ public static void AddBaseServices(this IServiceCollection services, IGlobalSett
services.AddScoped<IAuthRequestService, AuthRequestService>();
services.AddScoped<IDuoUniversalTokenService, DuoUniversalTokenService>();
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
services.AddLoginServices();
services.AddScoped<IOrganizationDomainService, OrganizationDomainService>();
services.AddVaultServices();
services.AddReportingServices();
Expand Down
224 changes: 224 additions & 0 deletions test/Core.Test/Auth/Entities/AuthRequestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
๏ปฟusing Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Xunit;

namespace Bit.Core.Test.Auth.Entities;

public class AuthRequestTests
{
[Fact]
public void IsValidForAuthentication_WithValidRequest_ReturnsTrue()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.True(result);
}

[Fact]
public void IsValidForAuthentication_WithWrongUserId_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var differentUserId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(differentUserId, accessCode);

// Assert
Assert.False(result, "Auth request should not validate for a different user");
}

[Fact]
public void IsValidForAuthentication_WithWrongAccessCode_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = "correct-code"
};

// Act
var result = authRequest.IsValidForAuthentication(userId, "wrong-code");

// Assert
Assert.False(result);
}

[Fact]
public void IsValidForAuthentication_WithoutResponseDate_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = null, // Not responded to
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Unanswered auth requests should not be valid");
}

[Fact]
public void IsValidForAuthentication_WithApprovedFalse_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = false, // Denied
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Denied auth requests should not be valid");
}

[Fact]
public void IsValidForAuthentication_WithApprovedNull_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = null, // Pending
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Pending auth requests should not be valid");
}

[Fact]
public void IsValidForAuthentication_WithExpiredRequest_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow.AddMinutes(-20), // Expired (15 min timeout)
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Expired auth requests should not be valid");
}

[Fact]
public void IsValidForAuthentication_WithWrongType_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.Unlock, // Wrong type
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = null,
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Only AuthenticateAndUnlock type should be valid");
}

[Fact]
public void IsValidForAuthentication_WithAlreadyUsed_ReturnsFalse()
{
// Arrange
var userId = Guid.NewGuid();
var accessCode = "test-access-code";
var authRequest = new AuthRequest
{
UserId = userId,
Type = AuthRequestType.AuthenticateAndUnlock,
ResponseDate = DateTime.UtcNow,
Approved = true,
CreationDate = DateTime.UtcNow,
AuthenticationDate = DateTime.UtcNow, // Already used
AccessCode = accessCode
};

// Act
var result = authRequest.IsValidForAuthentication(userId, accessCode);

// Assert
Assert.False(result, "Auth requests should only be valid for one-time use");
}
}
Loading