1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-27 13:05:23 +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:
Ike 2024-09-05 11:17:15 -07:00 committed by GitHub
parent 64a7cba013
commit fa5d6712c5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 863 additions and 1 deletions

View File

@ -101,7 +101,7 @@ public abstract class BaseRequestValidator<T> where T : class
protected async Task ValidateAsync(T context, ValidatedTokenRequest request, protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
CustomValidatorRequestContext validatorContext) CustomValidatorRequestContext validatorContext)
{ {
var isBot = (validatorContext.CaptchaResponse?.IsBot ?? false); var isBot = validatorContext.CaptchaResponse?.IsBot ?? false;
if (isBot) if (isBot)
{ {
_logger.LogInformation(Constants.BypassFiltersEventId, _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) private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user)
{ {
var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts;

View File

@ -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,
};
}
}

View 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();
}
}

View 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>>>());
}
}

View 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);
}
}