mirror of
https://github.com/bitwarden/server.git
synced 2024-11-24 12:35:25 +01:00
[PM-6664] Base Request Validator Unit Tests and Resource Owner integration Tests (#4582)
* intial commit * Some UnitTests for the VerifyAsync flows * WIP org two factor * removed useless tests * added ResourceOwnerValidation integration tests * fixing formatting * addressing comments * removed comment
This commit is contained in:
parent
64a7cba013
commit
fa5d6712c5
@ -101,7 +101,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
var isBot = (validatorContext.CaptchaResponse?.IsBot ?? false);
|
||||
var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
|
||||
if (isBot)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
@ -621,6 +621,13 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// checks to see if a user is trying to log into a new device
|
||||
/// and has reached the maximum number of failed login attempts.
|
||||
/// </summary>
|
||||
/// <param name="unknownDevice">boolean</param>
|
||||
/// <param name="user">current user</param>
|
||||
/// <returns></returns>
|
||||
private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
|
||||
{
|
||||
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;
|
||||
|
@ -0,0 +1,272 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Identity.Models.Request.Accounts;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Identity.IntegrationTest.RequestValidation;
|
||||
|
||||
public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplicationFactory>
|
||||
{
|
||||
private const string DefaultPassword = "master_password_hash";
|
||||
private const string DefaultUsername = "test@email.qa";
|
||||
private const string DefaultDeviceIdentifier = "test_identifier";
|
||||
private readonly IdentityApplicationFactory _factory;
|
||||
private readonly UserManager<User> _userManager;
|
||||
private readonly IAuthRequestRepository _authRequestRepository;
|
||||
|
||||
public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory)
|
||||
{
|
||||
_factory = factory;
|
||||
|
||||
_userManager = _factory.GetService<UserManager<User>>();
|
||||
_authRequestRepository = _factory.GetService<IAuthRequestRepository>();
|
||||
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_Success()
|
||||
{
|
||||
// Arrange
|
||||
await EnsureUserCreatedAsync();
|
||||
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(),
|
||||
context => context.SetAuthEmail(DefaultUsername));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString();
|
||||
Assert.NotNull(token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_AuthEmailHeaderInvalid_InvalidGrantResponse()
|
||||
{
|
||||
// Arrange
|
||||
await EnsureUserCreatedAsync();
|
||||
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync(
|
||||
"/connect/token",
|
||||
GetFormUrlEncodedContent()
|
||||
);
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
|
||||
Assert.Equal("Auth-Email header invalid.", error);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_UserNull_Failure(string username)
|
||||
{
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(username: username),
|
||||
context => context.SetAuthEmail(username));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
|
||||
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// I would have liked to spy into the IUserService but by spying into the IUserService it
|
||||
/// creates a Singleton that is not available to the UserManager<User> thus causing the
|
||||
/// RegisterAsync() to create a the user in a different UserStore than the one the
|
||||
/// UserManager<User> has access to. This is an assumption made from observing the behavior while
|
||||
/// writing theses tests. I could be wrong.
|
||||
///
|
||||
/// For the time being, verifying that the user is not null confirms that the failure is due to
|
||||
/// a bad password.
|
||||
/// </summary>
|
||||
/// <param name="badPassword">random password</param>
|
||||
/// <returns></returns>
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_BadPassword_Failure(string badPassword)
|
||||
{
|
||||
// Arrange
|
||||
await EnsureUserCreatedAsync();
|
||||
|
||||
// Verify the User is not null to ensure the failure is due to bad password
|
||||
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync("/connect/token",
|
||||
GetFormUrlEncodedContent(password: badPassword),
|
||||
context => context.SetAuthEmail(DefaultUsername));
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername));
|
||||
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
|
||||
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeLessThanOneHour_Success()
|
||||
{
|
||||
// Arrange
|
||||
// Ensure User
|
||||
await EnsureUserCreatedAsync();
|
||||
var user = await _userManager.FindByEmailAsync(DefaultUsername);
|
||||
Assert.NotNull(user);
|
||||
|
||||
// Connect Request to User and set CreationDate
|
||||
var authRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-30)
|
||||
);
|
||||
await _authRequestRepository.CreateAsync(authRequest);
|
||||
|
||||
var expectedAuthRequest = await _authRequestRepository.GetManyByUserIdAsync(user.Id);
|
||||
Assert.NotEmpty(expectedAuthRequest);
|
||||
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync("/connect/token",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "scope", "api offline_access" },
|
||||
{ "client_id", "web" },
|
||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||
{ "deviceIdentifier", DefaultDeviceIdentifier },
|
||||
{ "deviceName", "firefox" },
|
||||
{ "grant_type", "password" },
|
||||
{ "username", DefaultUsername },
|
||||
{ "password", DefaultPassword },
|
||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||
}), context => context.SetAuthEmail(DefaultUsername));
|
||||
|
||||
// Assert
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var token = AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String).GetString();
|
||||
Assert.NotNull(token);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ValidateAsync_ValidateContextAsync_AuthRequest_NotNull_AgeGreaterThanOneHour_Failure()
|
||||
{
|
||||
// Arrange
|
||||
// Ensure User
|
||||
await EnsureUserCreatedAsync(_factory);
|
||||
var user = await _userManager.FindByEmailAsync(DefaultUsername);
|
||||
Assert.NotNull(user);
|
||||
|
||||
// Create AuthRequest
|
||||
var authRequest = CreateAuthRequest(
|
||||
user.Id,
|
||||
AuthRequestType.AuthenticateAndUnlock,
|
||||
DateTime.UtcNow.AddMinutes(-61)
|
||||
);
|
||||
|
||||
// Act
|
||||
var context = await _factory.Server.PostAsync("/connect/token",
|
||||
new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "scope", "api offline_access" },
|
||||
{ "client_id", "web" },
|
||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||
{ "deviceIdentifier", DefaultDeviceIdentifier },
|
||||
{ "deviceName", "firefox" },
|
||||
{ "grant_type", "password" },
|
||||
{ "username", DefaultUsername },
|
||||
{ "password", DefaultPassword },
|
||||
{ "AuthRequest", authRequest.Id.ToString().ToLowerInvariant() }
|
||||
}), context => context.SetAuthEmail(DefaultUsername));
|
||||
|
||||
// Assert
|
||||
|
||||
/*
|
||||
An improvement on the current failure flow would be to document which part of
|
||||
the flow failed since all of the failures are basically the same.
|
||||
This doesn't build confidence in the tests.
|
||||
*/
|
||||
|
||||
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
|
||||
var root = body.RootElement;
|
||||
|
||||
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
|
||||
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
|
||||
}
|
||||
|
||||
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null)
|
||||
{
|
||||
factory ??= _factory;
|
||||
// No need to create more users than we need
|
||||
if (await _userManager.FindByEmailAsync(DefaultUsername) == null)
|
||||
{
|
||||
// Register user
|
||||
await factory.RegisterAsync(new RegisterRequestModel
|
||||
{
|
||||
Email = DefaultUsername,
|
||||
MasterPasswordHash = DefaultPassword
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private FormUrlEncodedContent GetFormUrlEncodedContent(
|
||||
string deviceId = null, string username = null, string password = null)
|
||||
{
|
||||
return new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
{ "scope", "api offline_access" },
|
||||
{ "client_id", "web" },
|
||||
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
|
||||
{ "deviceIdentifier", deviceId ?? DefaultDeviceIdentifier },
|
||||
{ "deviceName", "firefox" },
|
||||
{ "grant_type", "password" },
|
||||
{ "username", username ?? DefaultUsername },
|
||||
{ "password", password ?? DefaultPassword },
|
||||
});
|
||||
}
|
||||
|
||||
private static string DeviceTypeAsString(DeviceType deviceType)
|
||||
{
|
||||
return ((int)deviceType).ToString();
|
||||
}
|
||||
|
||||
private static AuthRequest CreateAuthRequest(
|
||||
Guid userId,
|
||||
AuthRequestType authRequestType,
|
||||
DateTime creationDate,
|
||||
bool? approved = null,
|
||||
DateTime? responseDate = null)
|
||||
{
|
||||
return new AuthRequest
|
||||
{
|
||||
UserId = userId,
|
||||
Type = authRequestType,
|
||||
Approved = approved,
|
||||
RequestDeviceIdentifier = DefaultDeviceIdentifier,
|
||||
RequestIpAddress = "1.1.1.1",
|
||||
AccessCode = DefaultPassword,
|
||||
PublicKey = "test_public_key",
|
||||
CreationDate = creationDate,
|
||||
ResponseDate = responseDate,
|
||||
};
|
||||
}
|
||||
}
|
31
test/Identity.Test/AutoFixture/RequestValidationFixtures.cs
Normal file
31
test/Identity.Test/AutoFixture/RequestValidationFixtures.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using System.Reflection;
|
||||
using AutoFixture;
|
||||
using AutoFixture.Xunit2;
|
||||
using Duende.IdentityServer.Validation;
|
||||
|
||||
namespace Bit.Identity.Test.AutoFixture;
|
||||
|
||||
internal class ValidatedTokenRequestCustomization : ICustomization
|
||||
{
|
||||
public ValidatedTokenRequestCustomization()
|
||||
{ }
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
{
|
||||
fixture.Customize<ValidatedTokenRequest>(composer => composer
|
||||
.With(o => o.RefreshToken, () => null)
|
||||
.With(o => o.ClientClaims, [])
|
||||
.With(o => o.Options, new Duende.IdentityServer.Configuration.IdentityServerOptions()));
|
||||
}
|
||||
}
|
||||
|
||||
public class ValidatedTokenRequestAttribute : CustomizeAttribute
|
||||
{
|
||||
public ValidatedTokenRequestAttribute()
|
||||
{ }
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new ValidatedTokenRequestCustomization();
|
||||
}
|
||||
}
|
400
test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs
Normal file
400
test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs
Normal file
@ -0,0 +1,400 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Bit.Identity.Test.Wrappers;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using AuthFixtures = Bit.Identity.Test.AutoFixture;
|
||||
|
||||
|
||||
namespace Bit.Identity.Test.IdentityServer;
|
||||
|
||||
public class BaseRequestValidatorTests
|
||||
{
|
||||
private UserManager<User> _userManager;
|
||||
private readonly IDeviceRepository _deviceRepository;
|
||||
private readonly IDeviceService _deviceService;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
|
||||
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly ILogger<BaseRequestValidatorTests> _logger;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _tokenDataFactory;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
|
||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||
|
||||
public BaseRequestValidatorTests()
|
||||
{
|
||||
_deviceRepository = Substitute.For<IDeviceRepository>();
|
||||
_deviceService = Substitute.For<IDeviceService>();
|
||||
_userService = Substitute.For<IUserService>();
|
||||
_eventService = Substitute.For<IEventService>();
|
||||
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
|
||||
_duoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
|
||||
_organizationRepository = Substitute.For<IOrganizationRepository>();
|
||||
_organizationUserRepository = Substitute.For<IOrganizationUserRepository>();
|
||||
_applicationCacheService = Substitute.For<IApplicationCacheService>();
|
||||
_mailService = Substitute.For<IMailService>();
|
||||
_logger = Substitute.For<ILogger<BaseRequestValidatorTests>>();
|
||||
_currentContext = Substitute.For<ICurrentContext>();
|
||||
_globalSettings = Substitute.For<GlobalSettings>();
|
||||
_userRepository = Substitute.For<IUserRepository>();
|
||||
_policyService = Substitute.For<IPolicyService>();
|
||||
_tokenDataFactory = Substitute.For<IDataProtectorTokenFactory<SsoEmail2faSessionTokenable>>();
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_userManager = SubstituteUserManager();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
_deviceRepository,
|
||||
_deviceService,
|
||||
_userService,
|
||||
_eventService,
|
||||
_organizationDuoWebTokenProvider,
|
||||
_duoWebV4SDKService,
|
||||
_organizationRepository,
|
||||
_organizationUserRepository,
|
||||
_applicationCacheService,
|
||||
_mailService,
|
||||
_logger,
|
||||
_currentContext,
|
||||
_globalSettings,
|
||||
_userRepository,
|
||||
_policyService,
|
||||
_tokenDataFactory,
|
||||
_featureService,
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> _Logger.LogInformation
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = true;
|
||||
_sut.isValid = true;
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
|
||||
// Assert
|
||||
await _eventService.Received(1)
|
||||
.LogUserEventAsync(context.CustomValidatorRequestContext.User.Id,
|
||||
Core.Enums.EventType.User_FailedLogIn);
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
(self hosted) |-> _logger.LogWarning()
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ContextNotValid_SelfHosted_ShouldBuildErrorResult_ShouldLogWarning(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings());
|
||||
_globalSettings.SelfHosted = true;
|
||||
_sut.isValid = false;
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
_logger.Received(1).LogWarning(Constants.BypassFiltersEventId, "Failed login attempt. ");
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync
|
||||
|-> BuildErrorResultAsync -> _eventService.LogUserEventAsync
|
||||
|-> SetErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
// This needs to be n-1 of the max failed login attempts
|
||||
context.CustomValidatorRequestContext.User.FailedLoginCount = 2;
|
||||
context.CustomValidatorRequestContext.KnownDevice = false;
|
||||
|
||||
_globalSettings.Captcha.Returns(
|
||||
new GlobalSettings.CaptchaSettings
|
||||
{
|
||||
MaximumFailedLoginAttempts = 3
|
||||
});
|
||||
_sut.isValid = false;
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _mailService.Received(1)
|
||||
.SendFailedLoginAttemptsEmailAsync(
|
||||
Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message);
|
||||
}
|
||||
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildErrorResult
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_AuthCodeGrantType_DeviceNull_ShouldError(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "authorization_code";
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
Assert.Equal("No device information provided.", errorResponse.Message);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
|
||||
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
|
||||
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
|
||||
context.CustomValidatorRequestContext.User.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>()
|
||||
);
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsValidAuthTypeAsync -> SaveDeviceAsync -> BuildSuccessResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.CustomValidatorRequestContext.User.CreationDate = DateTime.UtcNow - TimeSpan.FromDays(1);
|
||||
_globalSettings.DisableEmailNewDevice = false;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
|
||||
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
|
||||
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
|
||||
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
|
||||
_deviceRepository.GetByIdentifierAsync("DeviceIdentifier", Arg.Any<Guid>())
|
||||
.Returns(new Device() { Identifier = "DeviceIdentifier" });
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _eventService.LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
ValidateAsync -> IsLegacyUser -> BuildErrorResultAsync
|
||||
*/
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_InvalidAuthType_ShouldSetSsoResult(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
|
||||
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
|
||||
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
|
||||
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = "";
|
||||
|
||||
_policyService.AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed)
|
||||
.Returns(Task.FromResult(true));
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ValidateAsync_IsLegacyUser_FailAuthForLegacyUserAsync(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
var user = context.CustomValidatorRequestContext.User;
|
||||
user.Key = null;
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
context.ValidatedTokenRequest.ClientId = "Not Web";
|
||||
_sut.isValid = true;
|
||||
_featureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers).Returns(true);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal($"Encryption key migration is required. Please log in to the web vault at {_globalSettings.BaseServiceUri.VaultWithHash}"
|
||||
, errorResponse.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RequiresTwoFactorAsync_ClientCredentialsGrantType_ShouldReturnFalse(
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
context.ValidatedTokenRequest.GrantType = "client_credentials";
|
||||
|
||||
// Act
|
||||
var result = await _sut.TestRequiresTwoFactorAsync(
|
||||
context.CustomValidatorRequestContext.User,
|
||||
context.ValidatedTokenRequest);
|
||||
|
||||
// Assert
|
||||
Assert.False(result.Item1);
|
||||
Assert.Null(result.Item2);
|
||||
}
|
||||
|
||||
private BaseRequestValidationContextFake CreateContext(
|
||||
ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
return new BaseRequestValidationContextFake(
|
||||
tokenRequest,
|
||||
requestContext,
|
||||
grantResult
|
||||
);
|
||||
}
|
||||
|
||||
private UserManager<User> SubstituteUserManager()
|
||||
{
|
||||
return new UserManager<User>(Substitute.For<IUserStore<User>>(),
|
||||
Substitute.For<IOptions<IdentityOptions>>(),
|
||||
Substitute.For<IPasswordHasher<User>>(),
|
||||
Enumerable.Empty<IUserValidator<User>>(),
|
||||
Enumerable.Empty<IPasswordValidator<User>>(),
|
||||
Substitute.For<ILookupNormalizer>(),
|
||||
Substitute.For<IdentityErrorDescriber>(),
|
||||
Substitute.For<IServiceProvider>(),
|
||||
Substitute.For<ILogger<UserManager<User>>>());
|
||||
}
|
||||
}
|
152
test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs
Normal file
152
test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs
Normal file
@ -0,0 +1,152 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tokens;
|
||||
using Bit.Identity.IdentityServer;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Identity.Test.Wrappers;
|
||||
|
||||
public class BaseRequestValidationContextFake
|
||||
{
|
||||
public ValidatedTokenRequest ValidatedTokenRequest;
|
||||
public CustomValidatorRequestContext CustomValidatorRequestContext;
|
||||
public GrantValidationResult GrantResult;
|
||||
|
||||
public BaseRequestValidationContextFake(
|
||||
ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext customValidatorRequestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
ValidatedTokenRequest = tokenRequest;
|
||||
CustomValidatorRequestContext = customValidatorRequestContext;
|
||||
GrantResult = grantResult;
|
||||
}
|
||||
}
|
||||
|
||||
interface IBaseRequestValidatorTestWrapper
|
||||
{
|
||||
Task ValidateAsync(BaseRequestValidationContextFake context);
|
||||
}
|
||||
|
||||
public class BaseRequestValidatorTestWrapper : BaseRequestValidator<BaseRequestValidationContextFake>,
|
||||
IBaseRequestValidatorTestWrapper
|
||||
{
|
||||
|
||||
/*
|
||||
* Some of the logic trees call `ValidateContextAsync`. Since this is a test wrapper, we set the return value
|
||||
* of ValidateContextAsync() to whatever we need for the specific test case.
|
||||
*/
|
||||
public bool isValid { get; set; }
|
||||
public BaseRequestValidatorTestWrapper(
|
||||
UserManager<User> userManager,
|
||||
IDeviceRepository deviceRepository,
|
||||
IDeviceService deviceService,
|
||||
IUserService userService,
|
||||
IEventService eventService,
|
||||
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
|
||||
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
IMailService mailService,
|
||||
ILogger logger,
|
||||
ICurrentContext currentContext,
|
||||
GlobalSettings globalSettings,
|
||||
IUserRepository userRepository,
|
||||
IPolicyService policyService,
|
||||
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
|
||||
base(
|
||||
userManager,
|
||||
deviceRepository,
|
||||
deviceService,
|
||||
userService,
|
||||
eventService,
|
||||
organizationDuoWebTokenProvider,
|
||||
duoWebV4SDKService,
|
||||
organizationRepository,
|
||||
organizationUserRepository,
|
||||
applicationCacheService,
|
||||
mailService,
|
||||
logger,
|
||||
currentContext,
|
||||
globalSettings,
|
||||
userRepository,
|
||||
policyService,
|
||||
tokenDataFactory,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task ValidateAsync(
|
||||
BaseRequestValidationContextFake context)
|
||||
{
|
||||
await ValidateAsync(context, context.ValidatedTokenRequest, context.CustomValidatorRequestContext);
|
||||
}
|
||||
|
||||
public async Task<Tuple<bool, Organization>> TestRequiresTwoFactorAsync(
|
||||
User user,
|
||||
ValidatedTokenRequest context)
|
||||
{
|
||||
return await RequiresTwoFactorAsync(user, context);
|
||||
}
|
||||
|
||||
protected override ClaimsPrincipal GetSubject(
|
||||
BaseRequestValidationContextFake context)
|
||||
{
|
||||
return context.ValidatedTokenRequest.Subject ?? new ClaimsPrincipal();
|
||||
}
|
||||
|
||||
protected override void SetErrorResult(
|
||||
BaseRequestValidationContextFake context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.GrantResult = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse);
|
||||
}
|
||||
|
||||
protected override void SetSsoResult(
|
||||
BaseRequestValidationContextFake context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.GrantResult = new GrantValidationResult(
|
||||
TokenRequestErrors.InvalidGrant, "Sso authentication required.", customResponse);
|
||||
}
|
||||
|
||||
protected override Task SetSuccessResult(
|
||||
BaseRequestValidationContextFake context,
|
||||
User user,
|
||||
List<Claim> claims,
|
||||
Dictionary<string, object> customResponse)
|
||||
{
|
||||
context.GrantResult = new GrantValidationResult(customResponse: customResponse);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
protected override void SetTwoFactorResult(
|
||||
BaseRequestValidationContextFake context,
|
||||
Dictionary<string, object> customResponse)
|
||||
{ }
|
||||
|
||||
protected override Task<bool> ValidateContextAsync(
|
||||
BaseRequestValidationContextFake context,
|
||||
CustomValidatorRequestContext validatorContext)
|
||||
{
|
||||
return Task.FromResult(isValid);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user