1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-02 00:22:50 -05:00

Merge branch 'master' into feature/flexible-collections

This commit is contained in:
Vincent Salucci
2023-08-24 10:28:10 -05:00
63 changed files with 3154 additions and 9504 deletions

View File

@ -16,6 +16,34 @@ namespace Bit.Api.Test.Controllers;
[SutProviderCustomize]
public class OrganizationUsersControllerTests
{
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_InivitedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{
orgUser.Status = Core.Enums.OrganizationUserStatusType.Invited;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(1).AcceptUserAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
[BitAutoData]
public async Task PutResetPasswordEnrollment_ConfirmedUser_AcceptsInvite(Guid orgId, Guid userId, OrganizationUserResetPasswordEnrollmentRequestModel model,
User user, OrganizationUser orgUser, SutProvider<OrganizationUsersController> sutProvider)
{
orgUser.Status = Core.Enums.OrganizationUserStatusType.Confirmed;
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IOrganizationUserRepository>().GetByOrganizationAsync(default, default).ReturnsForAnyArgs(orgUser);
await sutProvider.Sut.PutResetPasswordEnrollment(orgId, userId, model);
await sutProvider.GetDependency<IOrganizationService>().Received(0).AcceptUserAsync(orgId, user, sutProvider.GetDependency<IUserService>());
}
[Theory]
[BitAutoData]
public async Task Accept_RequiresKnownUser(Guid orgId, Guid orgUserId, OrganizationUserAcceptRequestModel model,

View File

@ -1,4 +1,6 @@
using System.Reflection;
#nullable enable
using System.Reflection;
using AutoFixture;
using Bit.Test.Common.Helpers;
using Xunit.Sdk;
@ -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(params object[] fixedTestParameters) :
public BitAutoDataAttribute() : this(Array.Empty<object>()) { }
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);
}

View File

@ -103,6 +103,7 @@ public static class AssertHelper
public static Expression<Predicate<T>> AssertEqualExpected<T>(T expected) =>
(T actual) => AssertEqualExpectedPredicate(expected)(actual);
[StackTraceHidden]
public static JsonElement AssertJsonProperty(JsonElement element, string propertyName, JsonValueKind jsonValueKind)
{
if (!element.TryGetProperty(propertyName, out var subElement))

View File

@ -1,4 +1,7 @@
using System.Reflection;
#nullable enable
using System.ComponentModel;
using System.Reflection;
using AutoFixture;
using AutoFixture.Kernel;
using AutoFixture.Xunit2;
@ -8,18 +11,23 @@ namespace Bit.Test.Common.Helpers;
public static class BitAutoDataAttributeHelpers
{
public static IEnumerable<object[]> GetData(MethodInfo testMethod, IFixture fixture, object[] fixedTestParameters)
public static IEnumerable<object?[]> GetData(MethodInfo testMethod, IFixture fixture, object?[] fixedTestParameters)
{
var methodParameters = testMethod.GetParameters();
var classCustomizations = testMethod.DeclaringType.GetCustomAttributes<BitCustomizeAttribute>().Select(attr => attr.GetCustomization());
// We aren't worried about a test method not having a class it belongs to.
var classCustomizations = testMethod.DeclaringType!.GetCustomAttributes<BitCustomizeAttribute>().Select(attr => attr.GetCustomization());
var methodCustomizations = testMethod.GetCustomAttributes<BitCustomizeAttribute>().Select(attr => attr.GetCustomization());
fixedTestParameters = fixedTestParameters ?? Array.Empty<object>();
fixedTestParameters ??= Array.Empty<object>();
fixture = ApplyCustomizations(ApplyCustomizations(fixture, classCustomizations), methodCustomizations);
// The first n number of parameters should be match to the supplied parameters
var fixedTestInputParameters = methodParameters.Take(fixedTestParameters.Length).Zip(fixedTestParameters);
var missingParameters = methodParameters.Skip(fixedTestParameters.Length).Select(p => CustomizeAndCreate(p, fixture));
return new object[1][] { fixedTestParameters.Concat(missingParameters).ToArray() };
return new object?[1][] { ConvertFixedParameters(fixedTestInputParameters.ToArray()).Concat(missingParameters).ToArray() };
}
public static object CustomizeAndCreate(ParameterInfo p, IFixture fixture)
@ -48,4 +56,71 @@ public static class BitAutoDataAttributeHelpers
return newFixture;
}
public static IEnumerable<object?> ConvertFixedParameters((ParameterInfo Parameter, object? Value)[] fixedParameters)
{
var output = new object?[fixedParameters.Length];
for (var i = 0; i < fixedParameters.Length; i++)
{
var (parameter, value) = fixedParameters[i];
// If the value is null, just return the value
if (value is null || value.GetType() == parameter.ParameterType)
{
output[i] = value;
continue;
}
// If the value is a string and it's not a perfect match, try to convert it.
if (value is string stringValue)
{
//
if (parameter.ParameterType.IsGenericType && parameter.ParameterType.GetGenericTypeDefinition() == typeof(Nullable<>))
{
if (TryConvertToType(stringValue, Nullable.GetUnderlyingType(parameter.ParameterType)!, out var nullableConvertedValue))
{
output[i] = nullableConvertedValue;
continue;
}
// We couldn't convert it, so set it as the input value and let XUnit throw
output[i] = value;
continue;
}
if (TryConvertToType(stringValue, parameter.ParameterType, out var convertedValue))
{
output[i] = convertedValue;
continue;
}
// We couldn't convert it, so set it as the input value and let XUnit throw
output[i] = value;
}
// No easy conversion, give them back the value
output[i] = value;
}
return output;
}
private static bool TryConvertToType(string value, Type destinationType, out object? convertedValue)
{
convertedValue = null;
if (string.IsNullOrEmpty(value))
{
return false;
}
var converter = TypeDescriptor.GetConverter(destinationType);
if (converter.CanConvertFrom(typeof(string)))
{
convertedValue = converter.ConvertFromInvariantString(value);
return true;
}
return false;
}
}

View File

@ -1,4 +1,5 @@
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.Auth.Services.Implementations;
@ -11,6 +12,7 @@ using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using NSubstitute;
using Xunit;
@ -69,12 +71,23 @@ public class AuthRequestServiceTests
Assert.Null(foundAuthRequest);
}
[Theory, BitAutoData]
/// <summary>
/// Story: AdminApproval AuthRequests should have a longer expiration time by default and non-AdminApproval ones
/// should expire after 15 minutes by default.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AdminApproval, "-10.00:00:00")]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
[BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
public async Task GetValidatedAuthRequestAsync_IfExpired_ReturnsNull(
AuthRequestType authRequestType,
TimeSpan creationTimeBeforeNow,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddHours(-1);
authRequest.Type = authRequestType;
authRequest.CreationDate = DateTime.UtcNow.Add(creationTimeBeforeNow);
authRequest.Approved = false;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
@ -85,6 +98,29 @@ public class AuthRequestServiceTests
Assert.Null(foundAuthRequest);
}
/// <summary>
/// Story: Once a AdminApproval type has been approved it has a different expiration time based on time
/// after the response.
/// </summary>
[Theory]
[BitAutoData]
public async Task GetValidatedAuthRequestAsync_AdminApprovalApproved_HasLongerExpiration_ReturnsRequest(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.Type = AuthRequestType.AdminApproval;
authRequest.Approved = true;
authRequest.ResponseDate = DateTime.UtcNow.Add(TimeSpan.FromHours(-13));
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
var validatedAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);
Assert.Null(validatedAuthRequest);
}
[Theory, BitAutoData]
public async Task GetValidatedAuthRequestAsync_IfValid_ReturnsAuthRequest(
SutProvider<AuthRequestService> sutProvider,
@ -96,6 +132,10 @@ public class AuthRequestServiceTests
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
var foundAuthRequest = await sutProvider.Sut.GetValidatedAuthRequestAsync(authRequest.Id, authRequest.AccessCode);
Assert.NotNull(foundAuthRequest);
@ -136,13 +176,22 @@ public class AuthRequestServiceTests
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
}
[Theory, BitAutoData]
/// <summary>
/// Story: Non-AdminApproval requests should be created without a known device if the settings is set to <c>false</c>
/// Non-AdminApproval ones should also have a push notification sent about them.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
[BitAutoData(AuthRequestType.Unlock)]
[BitAutoData(new object?[1] { null })]
public async Task CreateAuthRequestAsync_CreatesAuthRequest(
AuthRequestType? authRequestType,
SutProvider<AuthRequestService> sutProvider,
AuthRequestCreateRequestModel createModel,
User user)
{
user.Email = createModel.Email;
createModel.Type = authRequestType;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(createModel.Email)
@ -152,28 +201,44 @@ public class AuthRequestServiceTests
.DeviceType
.Returns(DeviceType.Android);
sutProvider.GetDependency<ICurrentContext>()
.IpAddress
.Returns("1.1.1.1");
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth.KnownDevicesOnly
.Returns(false);
await sutProvider.Sut.CreateAuthRequestAsync(createModel);
sutProvider.GetDependency<IAuthRequestRepository>()
.CreateAsync(Arg.Any<AuthRequest>())
.Returns(c => c.ArgAt<AuthRequest>(0));
var createdAuthRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);
await sutProvider.GetDependency<IPushNotificationService>()
.Received()
.PushAuthRequestAsync(Arg.Any<AuthRequest>());
.PushAuthRequestAsync(createdAuthRequest);
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received()
.CreateAsync(Arg.Any<AuthRequest>());
.CreateAsync(createdAuthRequest);
}
[Theory, BitAutoData]
/// <summary>
/// Story: Since an AllowAnonymous endpoint calls this method we need
/// to verify that a device was able to be found via ICurrentContext
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
[BitAutoData(AuthRequestType.Unlock)]
public async Task CreateAuthRequestAsync_NoDeviceType_ThrowsBadRequest(
AuthRequestType authRequestType,
SutProvider<AuthRequestService> sutProvider,
AuthRequestCreateRequestModel createModel,
User user)
{
user.Email = createModel.Email;
createModel.Type = authRequestType;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(createModel.Email)
@ -186,13 +251,92 @@ public class AuthRequestServiceTests
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.CreateAuthRequestAsync(createModel));
}
/// <summary>
/// Story: If a user happens to exist to more than one organization, we will send the device approval request to
/// each of them.
/// </summary>
[Theory, BitAutoData]
public async Task CreateAuthRequestAsync_AdminApproval_CreatesForEachOrganization(
SutProvider<AuthRequestService> sutProvider,
AuthRequestCreateRequestModel createModel,
User user,
OrganizationUser organizationUser1,
OrganizationUser organizationUser2)
{
createModel.Type = AuthRequestType.AdminApproval;
user.Email = createModel.Email;
organizationUser1.UserId = user.Id;
organizationUser2.UserId = user.Id;
sutProvider.GetDependency<IUserRepository>()
.GetByEmailAsync(user.Email)
.Returns(user);
sutProvider.GetDependency<ICurrentContext>()
.DeviceType
.Returns(DeviceType.ChromeExtension);
sutProvider.GetDependency<ICurrentContext>()
.UserId
.Returns(user.Id);
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth.KnownDevicesOnly
.Returns(false);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(user.Id)
.Returns(new List<OrganizationUser>
{
organizationUser1,
organizationUser2,
});
sutProvider.GetDependency<IAuthRequestRepository>()
.CreateAsync(Arg.Any<AuthRequest>())
.Returns(c => c.ArgAt<AuthRequest>(0));
var authRequest = await sutProvider.Sut.CreateAuthRequestAsync(createModel);
Assert.Equal(organizationUser1.OrganizationId, authRequest.OrganizationId);
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received(1)
.CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser1.OrganizationId));
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received(1)
.CreateAsync(Arg.Is<AuthRequest>(o => o.OrganizationId == organizationUser2.OrganizationId));
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received(2)
.CreateAsync(Arg.Any<AuthRequest>());
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogUserEventAsync(user.Id, EventType.User_RequestedDeviceApproval);
}
/// <summary>
/// Story: When an <see cref="AuthRequest"> is approved we want to update it in the database so it cannot have
/// it's status changed again and we want to push a notification to let the user know of the approval.
/// In the case of the AdminApproval we also want to log an event.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
[BitAutoData(AuthRequestType.Unlock, null)]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
public async Task UpdateAuthRequestAsync_ValidResponse_SendsResponse(
AuthRequestType authRequestType,
Guid? organizationId,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = null;
authRequest.OrganizationId = organizationId;
authRequest.Type = authRequestType;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
@ -208,6 +352,18 @@ public class AuthRequestServiceTests
.GetByIdentifierAsync(device.Identifier, authRequest.UserId)
.Returns(device);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
.Returns(new OrganizationUser
{
UserId = authRequest.UserId,
OrganizationId = organizationId.GetValueOrDefault(),
});
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
@ -220,37 +376,75 @@ public class AuthRequestServiceTests
Assert.Equal("my_hash", udpatedAuthRequest.MasterPasswordHash);
// On approval, the response date should be set to current date
Assert.NotNull(udpatedAuthRequest.ResponseDate);
AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received()
.Received(1)
.ReplaceAsync(udpatedAuthRequest);
await sutProvider.GetDependency<IPushNotificationService>()
.Received()
.Received(1)
.PushAuthRequestResponseAsync(udpatedAuthRequest);
var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;
await sutProvider.GetDependency<IEventService>()
.Received(expectedNumberOfCalls)
.LogOrganizationUserEventAsync(
Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
EventType.OrganizationUser_ApprovedAuthRequest);
}
[Theory, BitAutoData]
/// <summary>
/// Story: When an <see cref="AuthRequest"> is rejected we want to update it in the database so it cannot have
/// it's status changed again but we do not want to send a push notification to the original device
/// so as to not leak that it was rejected. In the case of an AdminApproval type we do want to log an event though
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AdminApproval, "7b055ea1-38be-42d0-b2e4-becb2340f8df")]
[BitAutoData(AuthRequestType.Unlock, null)]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, null)]
public async Task UpdateAuthRequestAsync_ResponseNotApproved_DoesNotLeakRejection(
AuthRequestType authRequestType,
Guid? organizationId,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
// Give it a recent creation time which is valid for all types of AuthRequests
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Type = authRequestType;
// Has not been decided already
authRequest.Approved = null;
authRequest.OrganizationId = organizationId;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
// Setup a device for all requests even though it will not be called for verification in a AdminApproval
var device = new Device
{
Id = Guid.NewGuid(),
Identifier = "test_identifier",
};
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
sutProvider.GetDependency<IDeviceRepository>()
.GetByIdentifierAsync(device.Identifier, authRequest.UserId)
.Returns(device);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(Arg.Any<Guid>(), Arg.Any<Guid>())
.Returns(new OrganizationUser
{
UserId = authRequest.UserId,
OrganizationId = organizationId.GetValueOrDefault(),
});
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
@ -262,6 +456,9 @@ public class AuthRequestServiceTests
var udpatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);
Assert.Equal(udpatedAuthRequest.MasterPasswordHash, authRequest.MasterPasswordHash);
Assert.False(udpatedAuthRequest.Approved);
Assert.NotNull(udpatedAuthRequest.ResponseDate);
AssertHelper.AssertRecent(udpatedAuthRequest.ResponseDate!.Value);
await sutProvider.GetDependency<IAuthRequestRepository>()
.Received()
@ -270,17 +467,37 @@ public class AuthRequestServiceTests
await sutProvider.GetDependency<IPushNotificationService>()
.DidNotReceiveWithAnyArgs()
.PushAuthRequestResponseAsync(udpatedAuthRequest);
var expectedNumberOfCalls = organizationId.HasValue ? 1 : 0;
await sutProvider.GetDependency<IEventService>()
.Received(expectedNumberOfCalls)
.LogOrganizationUserEventAsync(
Arg.Is<OrganizationUser>(ou => ou.UserId == authRequest.UserId && ou.OrganizationId == organizationId),
EventType.OrganizationUser_RejectedAuthRequest);
}
[Theory, BitAutoData]
/// <summary>
/// Story: A bad actor is able to get ahold of the request id of a valid <see cref="AuthRequest" />
/// and tries to approve it from their own Bitwarden account. We need to validate that the currently signed in user
/// is the same user that originally created the request and we want to pretend it does not exist at all by throwing
/// NotFoundException.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
[BitAutoData(AuthRequestType.Unlock)]
public async Task UpdateAuthRequestAsync_InvalidUser_ThrowsNotFound(
AuthRequestType authRequestType,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest,
Guid userId)
Guid authenticatedUserId)
{
// Give it a recent creation date so that it is valid
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = false;
// The request hasn't been Approved/Disapproved already
authRequest.Approved = null;
// Has an type that needs the UserId property validated
authRequest.Type = authRequestType;
// Auth request should not be null
sutProvider.GetDependency<IAuthRequestRepository>()
@ -297,23 +514,39 @@ public class AuthRequestServiceTests
// Give it a randomly generated userId such that it won't be valid for the AuthRequest
await Assert.ThrowsAsync<NotFoundException>(
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, userId, updateModel));
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authenticatedUserId, updateModel));
}
[Theory, BitAutoData]
/// <summary>
/// Story: A user created this auth request and does not approve/reject the request
/// for 16 minutes, which is past the default expiration time. This auth request
/// will be purged from the database soon but might exist for some amount of time after it's expiration
/// this method should throw a NotFoundException since it theoretically should not exist, this
/// could be a user finally clicking Approve after the request sitting on their phone for a while.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock, "-00:16:00")]
[BitAutoData(AuthRequestType.Unlock, "-00:16:00")]
[BitAutoData(AuthRequestType.AdminApproval, "-8.00:00:00")]
public async Task UpdateAuthRequestAsync_OldAuthRequest_ThrowsNotFound(
AuthRequestType authRequestType,
TimeSpan timeBeforeCreation,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
// AuthRequest's have a valid lifetime of only 15 minutes, make it older than that
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-16);
authRequest.Approved = false;
// AuthRequest's have a default valid lifetime of only 15 minutes, make it older than that
authRequest.CreationDate = DateTime.UtcNow.Add(timeBeforeCreation);
// Make it so that the user has not made a decision on this request
authRequest.Approved = null;
// Make it one of the types that doesn't have longer expiration i.e AdminApproval
authRequest.Type = authRequestType;
// Auth request should not be null
// The item should still exist in the database
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
// Represents the user finally clicking approve.
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
@ -322,27 +555,38 @@ public class AuthRequestServiceTests
MasterPasswordHash = "my_hash",
};
// Give it a randomly generated userId such that it won't be valid for the AuthRequest
await Assert.ThrowsAsync<NotFoundException>(
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
}
[Theory, BitAutoData]
/// <summary>
/// Story: non-AdminApproval types need to validate that the device used to respond to the
/// request is a known device to the authenticated user.
/// </summary>
[Theory]
[BitAutoData(AuthRequestType.AuthenticateAndUnlock)]
[BitAutoData(AuthRequestType.Unlock)]
public async Task UpdateAuthRequestAsync_InvalidDeviceIdentifier_ThrowsBadRequest(
AuthRequestType authRequestType,
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = null;
authRequest.Type = authRequestType;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
sutProvider.GetDependency<IDeviceRepository>()
.GetByIdentifierAsync(Arg.Any<string>(), authRequest.UserId)
.GetByIdentifierAsync("invalid_identifier", authRequest.UserId)
.Returns((Device?)null);
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
@ -355,29 +599,21 @@ public class AuthRequestServiceTests
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
}
/// <summary>
/// Story: Once the destiny of an AuthRequest has been decided, it should be considered immutable
/// and new update request should be blocked.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_AlreadyApprovedOrRejected_ThrowsDuplicateAuthRequestException(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest)
{
// Set CreationDate to a valid recent value and Approved to a non-null value
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Approved = true;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
var device = new Device
{
Id = Guid.NewGuid(),
Identifier = "test_identifier",
};
sutProvider.GetDependency<IDeviceRepository>()
.GetByIdentifierAsync(device.Identifier, authRequest.UserId)
.Returns(device);
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
@ -390,4 +626,69 @@ public class AuthRequestServiceTests
async () => await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel));
}
/// <summary>
/// Story: An admin approves a request for one of their org users. For auditing purposes we need to
/// log an event that correlates the action for who the request was approved for. On approval we also need to
/// push the notification to the user.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_AdminApproved_LogsEvent(
SutProvider<AuthRequestService> sutProvider,
AuthRequest authRequest,
OrganizationUser organizationUser)
{
authRequest.CreationDate = DateTime.UtcNow.AddMinutes(-10);
authRequest.Type = AuthRequestType.AdminApproval;
authRequest.OrganizationId = organizationUser.OrganizationId;
authRequest.Approved = null;
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequest.Id)
.Returns(authRequest);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByOrganizationAsync(authRequest.OrganizationId!.Value, authRequest.UserId)
.Returns(organizationUser);
sutProvider.GetDependency<IGlobalSettings>()
.PasswordlessAuth
.Returns(new Settings.GlobalSettings.PasswordlessAuthSettings());
var updateModel = new AuthRequestUpdateRequestModel
{
Key = "test_key",
RequestApproved = true,
MasterPasswordHash = "my_hash",
};
var updatedAuthRequest = await sutProvider.Sut.UpdateAuthRequestAsync(authRequest.Id, authRequest.UserId, updateModel);
Assert.Equal("my_hash", updatedAuthRequest.MasterPasswordHash);
Assert.Equal("test_key", updatedAuthRequest.Key);
Assert.True(updatedAuthRequest.Approved);
Assert.NotNull(updatedAuthRequest.ResponseDate);
AssertHelper.AssertRecent(updatedAuthRequest.ResponseDate!.Value);
await sutProvider.GetDependency<IEventService>()
.Received(1)
.LogOrganizationUserEventAsync(
Arg.Is(organizationUser), Arg.Is(EventType.OrganizationUser_ApprovedAuthRequest));
await sutProvider.GetDependency<IPushNotificationService>()
.Received(1)
.PushAuthRequestResponseAsync(authRequest);
}
[Theory, BitAutoData]
public async Task UpdateAuthRequestAsync_BadId_ThrowsNotFound(
SutProvider<AuthRequestService> sutProvider,
Guid authRequestId)
{
sutProvider.GetDependency<IAuthRequestRepository>()
.GetByIdAsync(authRequestId)
.Returns((AuthRequest?)null);
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAuthRequestAsync(
authRequestId, Guid.NewGuid(), new AuthRequestUpdateRequestModel()));
}
}

View File

@ -20,11 +20,11 @@ public class OrganizationCustomization : ICustomization
public void Customize(IFixture fixture)
{
var organizationId = Guid.NewGuid();
var maxConnections = (short)new Random().Next(10, short.MaxValue);
var maxCollections = (short)new Random().Next(10, short.MaxValue);
fixture.Customize<Organization>(composer => composer
.With(o => o.Id, organizationId)
.With(o => o.MaxCollections, maxConnections)
.With(o => o.MaxCollections, maxCollections)
.With(o => o.UseGroups, UseGroups));
fixture.Customize<Collection>(composer =>
@ -127,6 +127,24 @@ internal class OrganizationInvite : ICustomization
}
}
public class SecretsManagerOrganizationCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
var organizationId = Guid.NewGuid();
var planType = PlanType.EnterpriseAnnually;
fixture.Customize<Organization>(composer => composer
.With(o => o.Id, organizationId)
.With(o => o.UseSecretsManager, true)
.With(o => o.PlanType, planType)
.With(o => o.Plan, StaticStore.GetPasswordManagerPlan(planType).Name)
.With(o => o.MaxAutoscaleSmSeats, (int?)null)
.With(o => o.MaxAutoscaleSmServiceAccounts, (int?)null)
);
}
}
internal class OrganizationCustomizeAttribute : BitCustomizeAttribute
{
public bool UseGroups { get; set; }
@ -162,3 +180,9 @@ internal class OrganizationInviteCustomizeAttribute : BitCustomizeAttribute
PermissionsBlob = PermissionsBlob,
};
}
internal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttribute
{
public override ICustomization GetCustomization() =>
new SecretsManagerOrganizationCustomization();
}

View File

@ -0,0 +1,31 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Models.Business;
[SecretsManagerOrganizationCustomize]
public class SecretsManagerSubscriptionUpdateTests
{
[Theory]
[BitAutoData(PlanType.Custom)]
[BitAutoData(PlanType.FamiliesAnnually)]
[BitAutoData(PlanType.FamiliesAnnually2019)]
[BitAutoData(PlanType.EnterpriseMonthly2019)]
[BitAutoData(PlanType.EnterpriseAnnually2019)]
[BitAutoData(PlanType.TeamsMonthly2019)]
[BitAutoData(PlanType.TeamsAnnually2019)]
public async Task UpdateSubscriptionAsync_WithNonSecretsManagerPlanType_ThrowsBadRequestException(
PlanType planType,
Organization organization)
{
organization.PlanType = planType;
var exception = Assert.Throws<NotFoundException>(() => new SecretsManagerSubscriptionUpdate(organization, false));
Assert.Contains("Invalid Secrets Manager plan", exception.Message, StringComparison.InvariantCultureIgnoreCase);
}
}

View File

@ -1,12 +1,18 @@
using Bit.Core.Entities;
using System.Runtime.CompilerServices;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.Services;
[SutProviderCustomize]
public class DeviceServiceTests
{
[Fact]
@ -33,4 +39,227 @@ public class DeviceServiceTests
await pushRepo.Received().CreateOrUpdateRegistrationAsync("testtoken", id.ToString(),
userId.ToString(), "testid", DeviceType.Android);
}
/// <summary>
/// Story: A user choosed to keep trust in one of their current trusted devices, but not in another one of their
/// devices. We will rotate the trust of the currently signed in device as well as the device they chose but will
/// remove the trust of the device they didn't give new keys for.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_Works(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo,
Device deviceThree)
{
SetupOldTrust(deviceOne);
SetupOldTrust(deviceTwo);
SetupOldTrust(deviceThree);
deviceOne.Identifier = "current_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device>
{
deviceOne,
deviceTwo,
deviceThree,
});
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceTwo.Id,
EncryptedPublicKey = "encrypted_public_key_two",
EncryptedUserKey = "encrypted_user_key_two",
},
};
await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels);
// Updating trust, "current" or "other" only needs to change the EncryptedPublicKey & EncryptedUserKey
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceOne.Id &&
d.EncryptedPublicKey == "current_encrypted_public_key" &&
d.EncryptedUserKey == "current_encrypted_user_key" &&
d.EncryptedPrivateKey == "old_private_deviceOne"));
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceTwo.Id &&
d.EncryptedPublicKey == "encrypted_public_key_two" &&
d.EncryptedUserKey == "encrypted_user_key_two" &&
d.EncryptedPrivateKey == "old_private_deviceTwo"));
// Clearing trust should remove all key values
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceThree.Id &&
d.EncryptedPublicKey == null &&
d.EncryptedUserKey == null &&
d.EncryptedPrivateKey == null));
// Should have recieved a total of 3 calls, the ones asserted above
await sutProvider.GetDependency<IDeviceRepository>()
.Received(3)
.UpsertAsync(Arg.Any<Device>());
// TODO: .NET 8: Use nameof for parameter name.
static void SetupOldTrust(Device device, [CallerArgumentExpression("device")] string expression = null)
{
device.EncryptedPublicKey = $"old_public_{expression}";
device.EncryptedPrivateKey = $"old_private_{expression}";
device.EncryptedUserKey = $"old_user_{expression}";
}
}
/// <summary>
/// Story: This could result from a poor implementation of this method, if they attempt add trust to a device
/// that doesn't already have trust. They would have to create brand new values and for that values to be accurate
/// they would technically have all the values needed to trust a device, that is why we don't consider this bad
/// enough to throw but do skip it because we'd rather keep number of ways for trust to be added to the endpoint we
/// already have.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_DoesNotUpdateUntrustedDevices(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "current_device";
// Make deviceTwo untrusted
deviceTwo.EncryptedUserKey = string.Empty;
deviceTwo.EncryptedPublicKey = string.Empty;
deviceTwo.EncryptedPrivateKey = string.Empty;
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device>
{
deviceOne,
deviceTwo,
});
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceTwo.Id,
EncryptedPublicKey = "encrypted_public_key_two",
EncryptedUserKey = "encrypted_user_key_two",
},
};
await sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels);
// Check that UpsertAsync was called for the trusted device
await sutProvider.GetDependency<IDeviceRepository>()
.Received(1)
.UpsertAsync(Arg.Is<Device>(d =>
d.Id == deviceOne.Id &&
d.EncryptedPublicKey == "current_encrypted_public_key" &&
d.EncryptedUserKey == "current_encrypted_user_key"));
// Check that UpsertAsync was not called for the untrusted device
await sutProvider.GetDependency<IDeviceRepository>()
.DidNotReceive()
.UpsertAsync(Arg.Is<Device>(d => d.Id == deviceTwo.Id));
}
/// <summary>
/// Story: This should only happen if someone were to take the access token from a different device and try to rotate
/// a device that they don't actually have.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_ThrowsNotFoundException_WhenCurrentDeviceIdentifierDoesNotExist(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "some_other_device";
deviceTwo.Identifier = "another_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device>
{
deviceOne,
deviceTwo,
});
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
await Assert.ThrowsAsync<NotFoundException>(() =>
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel,
Enumerable.Empty<OtherDeviceKeysUpdateRequestModel>()));
}
/// <summary>
/// Story: This should only happen from a poorly implemented user of this method but important to enforce someone
/// using the method correctly, a device should only be rotated intentionally and including it as both the current
/// device and one of the users other device would mean they could rotate it twice and we aren't sure
/// which one they would want to win out.
/// </summary>
[Theory, BitAutoData]
public async Task UpdateDevicesTrustAsync_ThrowsBadRequestException_WhenCurrentDeviceIsIncludedInAlteredDevices(
SutProvider<DeviceService> sutProvider,
Guid currentUserId,
Device deviceOne,
Device deviceTwo)
{
deviceOne.Identifier = "current_device";
sutProvider.GetDependency<IDeviceRepository>()
.GetManyByUserIdAsync(currentUserId)
.Returns(new List<Device>
{
deviceOne,
deviceTwo,
});
var currentDeviceModel = new DeviceKeysUpdateRequestModel
{
EncryptedPublicKey = "current_encrypted_public_key",
EncryptedUserKey = "current_encrypted_user_key",
};
var alteredDeviceModels = new List<OtherDeviceKeysUpdateRequestModel>
{
new OtherDeviceKeysUpdateRequestModel
{
DeviceId = deviceOne.Id, // current device is included in alteredDevices
EncryptedPublicKey = "encrypted_public_key_one",
EncryptedUserKey = "encrypted_user_key_one",
},
};
await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.UpdateDevicesTrustAsync("current_device", currentUserId, currentDeviceModel, alteredDeviceModels));
}
}

View File

@ -406,7 +406,7 @@ public class PolicyServiceTests
[BitAutoData(true, false)]
[BitAutoData(false, true)]
[BitAutoData(false, false)]
public async Task SaveAsync_PolicyRequiredByTrustedDeviceEncryption_DisablePolicyOrDisableAutomaticEnrollment_ThrowsBadRequest(
public async Task SaveAsync_ResetPasswordPolicyRequiredByTrustedDeviceEncryption_DisablePolicyOrDisableAutomaticEnrollment_ThrowsBadRequest(
bool policyEnabled,
bool autoEnrollEnabled,
[PolicyFixtures.Policy(PolicyType.ResetPassword)] Policy policy,
@ -448,6 +448,43 @@ public class PolicyServiceTests
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_RequireSsoPolicyRequiredByTrustedDeviceEncryption_DisablePolicy_ThrowsBadRequest(
[PolicyFixtures.Policy(PolicyType.RequireSso)] Policy policy,
SutProvider<PolicyService> sutProvider)
{
policy.Enabled = false;
SetupOrg(sutProvider, policy.OrganizationId, new Organization
{
Id = policy.OrganizationId,
UsePolicies = true,
});
var ssoConfig = new SsoConfig { Enabled = true };
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
sutProvider.GetDependency<ISsoConfigRepository>()
.GetByOrganizationIdAsync(policy.OrganizationId)
.Returns(ssoConfig);
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
Assert.Contains("Trusted device encryption is on and requires this policy.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await sutProvider.GetDependency<IPolicyRepository>()
.DidNotReceiveWithAnyArgs()
.UpsertAsync(default);
await sutProvider.GetDependency<IEventService>()
.DidNotReceiveWithAnyArgs()
.LogPolicyEventAsync(default, default, default);
}
[Theory, BitAutoData]
public async Task SaveAsync_PolicyRequiredForAccountRecovery_NotEnabled_ThrowsBadRequestAsync(
[PolicyFixtures.Policy(Enums.PolicyType.ResetPassword)] Policy policy, SutProvider<PolicyService> sutProvider)
@ -624,12 +661,18 @@ public class PolicyServiceTests
private static void SetupUserPolicies(Guid userId, SutProvider<PolicyService> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByUserIdWithPolicyDetailsAsync(userId)
.GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.RequireSso)
.Returns(new List<OrganizationUserPolicyDetails>
{
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = false, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false},
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = false },
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = true },
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.RequireSso, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.Owner, OrganizationUserStatus = OrganizationUserStatusType.Confirmed, IsProvider = true }
});
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByUserIdWithPolicyDetailsAsync(userId, PolicyType.DisableSend)
.Returns(new List<OrganizationUserPolicyDetails>
{
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = false },
new() { OrganizationId = Guid.NewGuid(), PolicyType = PolicyType.DisableSend, PolicyEnabled = true, OrganizationUserType = OrganizationUserType.User, OrganizationUserStatus = OrganizationUserStatusType.Invited, IsProvider = true }
});

View File

@ -1,15 +1,23 @@
using System.Text.Json;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Services;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Fido2NetLib;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ReceivedExtensions;
using Xunit;
@ -134,7 +142,7 @@ public class UserServiceTests
}
[Theory, BitAutoData]
public async void HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)
public async Task HasPremiumFromOrganization_Returns_False_If_No_Orgs(SutProvider<UserService> sutProvider, User user)
{
sutProvider.GetDependency<IOrganizationUserRepository>().GetManyByUserAsync(user.Id).Returns(new List<OrganizationUser>());
Assert.False(await sutProvider.Sut.HasPremiumFromOrganization(user));
@ -144,7 +152,7 @@ public class UserServiceTests
[Theory]
[BitAutoData(false, true)]
[BitAutoData(true, false)]
public async void HasPremiumFromOrganization_Returns_False_If_Org_Not_Eligible(bool orgEnabled, bool orgUsersGetPremium, SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)
public async Task HasPremiumFromOrganization_Returns_False_If_Org_Not_Eligible(bool orgEnabled, bool orgUsersGetPremium, SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)
{
orgUser.OrganizationId = organization.Id;
organization.Enabled = orgEnabled;
@ -158,7 +166,7 @@ public class UserServiceTests
}
[Theory, BitAutoData]
public async void HasPremiumFromOrganization_Returns_True_If_Org_Eligible(SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)
public async Task HasPremiumFromOrganization_Returns_True_If_Org_Eligible(SutProvider<UserService> sutProvider, User user, OrganizationUser orgUser, Organization organization)
{
orgUser.OrganizationId = organization.Id;
organization.Enabled = true;
@ -170,4 +178,145 @@ public class UserServiceTests
Assert.True(await sutProvider.Sut.HasPremiumFromOrganization(user));
}
[Flags]
public enum ShouldCheck
{
Password = 0x1,
OTP = 0x2,
}
[Theory]
// A user who has a password, and the password is valid should only check for that password
[BitAutoData(true, "test_password", true, ShouldCheck.Password)]
// A user who does not have a password, should only check if the OTP is valid
[BitAutoData(false, "otp_token", true, ShouldCheck.OTP)]
// A user who has a password but supplied a OTP, it will check password first and then try OTP
[BitAutoData(true, "otp_token", true, ShouldCheck.Password | ShouldCheck.OTP)]
// A user who does not have a password and supplied an invalid OTP token, should only check OTP and return invalid
[BitAutoData(false, "bad_otp_token", false, ShouldCheck.OTP)]
// A user who does have a password but they supply a bad one, we will check both but it will still be invalid
[BitAutoData(true, "bad_test_password", false, ShouldCheck.Password | ShouldCheck.OTP)]
public async Task VerifySecretAsync_Works(
bool shouldHavePassword, string secret, bool expectedIsVerified, ShouldCheck shouldCheck, // inline theory data
SutProvider<UserService> sutProvider, User user) // AutoFixture injected data
{
// Arrange
var tokenProvider = SetupFakeTokenProvider(sutProvider, user);
SetupUserAndDevice(user, shouldHavePassword);
// Setup the fake password verification
var substitutedUserPasswordStore = Substitute.For<IUserPasswordStore<User>>();
substitutedUserPasswordStore
.GetPasswordHashAsync(user, Arg.Any<CancellationToken>())
.Returns((ci) =>
{
return Task.FromResult("hashed_test_password");
});
sutProvider.SetDependency<IUserStore<User>>(substitutedUserPasswordStore, "store");
sutProvider.GetDependency<IPasswordHasher<User>>("passwordHasher")
.VerifyHashedPassword(user, "hashed_test_password", "test_password")
.Returns((ci) =>
{
return PasswordVerificationResult.Success;
});
// HACK: SutProvider is being weird about not injecting the IPasswordHasher that I configured
var sut = new UserService(
sutProvider.GetDependency<IUserRepository>(),
sutProvider.GetDependency<ICipherRepository>(),
sutProvider.GetDependency<IOrganizationUserRepository>(),
sutProvider.GetDependency<IOrganizationRepository>(),
sutProvider.GetDependency<IMailService>(),
sutProvider.GetDependency<IPushNotificationService>(),
sutProvider.GetDependency<IUserStore<User>>(),
sutProvider.GetDependency<IOptions<IdentityOptions>>(),
sutProvider.GetDependency<IPasswordHasher<User>>(),
sutProvider.GetDependency<IEnumerable<IUserValidator<User>>>(),
sutProvider.GetDependency<IEnumerable<IPasswordValidator<User>>>(),
sutProvider.GetDependency<ILookupNormalizer>(),
sutProvider.GetDependency<IdentityErrorDescriber>(),
sutProvider.GetDependency<IServiceProvider>(),
sutProvider.GetDependency<ILogger<UserManager<User>>>(),
sutProvider.GetDependency<ILicensingService>(),
sutProvider.GetDependency<IEventService>(),
sutProvider.GetDependency<IApplicationCacheService>(),
sutProvider.GetDependency<IDataProtectionProvider>(),
sutProvider.GetDependency<IPaymentService>(),
sutProvider.GetDependency<IPolicyRepository>(),
sutProvider.GetDependency<IPolicyService>(),
sutProvider.GetDependency<IReferenceEventService>(),
sutProvider.GetDependency<IFido2>(),
sutProvider.GetDependency<ICurrentContext>(),
sutProvider.GetDependency<IGlobalSettings>(),
sutProvider.GetDependency<IOrganizationService>(),
sutProvider.GetDependency<IProviderUserRepository>(),
sutProvider.GetDependency<IStripeSyncService>());
var actualIsVerified = await sut.VerifySecretAsync(user, secret);
Assert.Equal(expectedIsVerified, actualIsVerified);
await tokenProvider
.Received(shouldCheck.HasFlag(ShouldCheck.OTP) ? 1 : 0)
.ValidateAsync(Arg.Any<string>(), secret, Arg.Any<UserManager<User>>(), user);
sutProvider.GetDependency<IPasswordHasher<User>>()
.Received(shouldCheck.HasFlag(ShouldCheck.Password) ? 1 : 0)
.VerifyHashedPassword(user, "hashed_test_password", secret);
}
private static void SetupUserAndDevice(User user,
bool shouldHavePassword)
{
if (shouldHavePassword)
{
user.MasterPassword = "test_password";
}
else
{
user.MasterPassword = null;
}
}
private static IUserTwoFactorTokenProvider<User> SetupFakeTokenProvider(SutProvider<UserService> sutProvider, User user)
{
var fakeUserTwoFactorProvider = Substitute.For<IUserTwoFactorTokenProvider<User>>();
fakeUserTwoFactorProvider
.GenerateAsync(Arg.Any<string>(), Arg.Any<UserManager<User>>(), user)
.Returns("OTP_TOKEN");
fakeUserTwoFactorProvider
.ValidateAsync(Arg.Any<string>(), Arg.Is<string>(s => s != "otp_token"), Arg.Any<UserManager<User>>(), user)
.Returns(false);
fakeUserTwoFactorProvider
.ValidateAsync(Arg.Any<string>(), "otp_token", Arg.Any<UserManager<User>>(), user)
.Returns(true);
sutProvider.GetDependency<IOptions<IdentityOptions>>()
.Value.Returns(new IdentityOptions
{
Tokens = new TokenOptions
{
ProviderMap = new Dictionary<string, TokenProviderDescriptor>()
{
["Email"] = new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<User>))
{
ProviderInstance = fakeUserTwoFactorProvider,
}
}
}
});
// The above arranging of dependencies is used in the constructor of UserManager
// ref: https://github.com/dotnet/aspnetcore/blob/bfeb3bf9005c36b081d1e48725531ee0e15a9dfb/src/Identity/Extensions.Core/src/UserManager.cs#L103-L120
// since the constructor of the Sut has ran already (when injected) I need to recreate it to get it to run again
sutProvider.Create();
return fakeUserTwoFactorProvider;
}
}

View File

@ -9,6 +9,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -33,33 +34,10 @@ public class IdentityServerSsoTests
public async Task Test_MasterPassword_DecryptionType()
{
// Arrange
var challenge = new string('c', 50);
var factory = await CreateFactoryAsync(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.MasterPassword,
}, challenge);
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
{ "twoFactorRemember", "0" },
{ "grant_type", "authorization_code" },
{ "code", "test_code" },
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}));
using var responseBody = await RunSuccessTestAsync(MemberDecryptionType.MasterPassword);
// Assert
// If the organization has a member decryption type of MasterPassword that should be the only option in the reply
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
@ -80,34 +58,11 @@ public class IdentityServerSsoTests
public async Task SsoLogin_TrustedDeviceEncryption_ReturnsOptions()
{
// Arrange
var challenge = new string('c', 50);
var factory = await CreateFactoryAsync(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}, challenge);
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
{ "twoFactorRemember", "0" },
{ "grant_type", "authorization_code" },
{ "code", "test_code" },
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}));
using var responseBody = await RunSuccessTestAsync(MemberDecryptionType.TrustedDeviceEncryption);
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
// they can decrypt with either option
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
@ -132,47 +87,25 @@ public class IdentityServerSsoTests
public async Task SsoLogin_TrustedDeviceEncryption_WithAdminResetPolicy_ReturnsOptions()
{
// Arrange
var challenge = new string('c', 50);
var factory = await CreateFactoryAsync(new SsoConfigurationData
using var responseBody = await RunSuccessTestAsync(async factory =>
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}, challenge);
var database = factory.GetDatabaseContext();
var database = factory.GetDatabaseContext();
var organization = await database.Organizations.SingleAsync();
var organization = await database.Organizations.SingleAsync();
var user = await database.Users.SingleAsync(u => u.Email == TestEmail);
var policyRepository = factory.Services.GetRequiredService<IPolicyRepository>();
await policyRepository.CreateAsync(new Policy
{
Type = PolicyType.ResetPassword,
Enabled = true,
Data = "{\"autoEnrollEnabled\": false }",
OrganizationId = organization.Id,
});
var organizationUser = await database.OrganizationUsers.SingleAsync(
ou => ou.OrganizationId == organization.Id && ou.UserId == user.Id);
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
{ "twoFactorRemember", "0" },
{ "grant_type", "authorization_code" },
{ "code", "test_code" },
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}));
organizationUser.ResetPasswordKey = "something";
await database.SaveChangesAsync();
}, MemberDecryptionType.TrustedDeviceEncryption);
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
// they can decrypt with either option
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
@ -183,7 +116,8 @@ public class IdentityServerSsoTests
// "Object": "userDecryptionOptions"
// "HasMasterPassword": true,
// "TrustedDeviceOption": {
// "HasAdminApproval": true
// "HasAdminApproval": true,
// "HasManageResetPasswordPermission": false
// }
// }
@ -196,6 +130,126 @@ public class IdentityServerSsoTests
[Fact]
public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_ReturnsOneOption()
{
using var responseBody = await RunSuccessTestAsync(async factory =>
{
await UpdateUserAsync(factory, user => user.MasterPassword = null);
}, MemberDecryptionType.TrustedDeviceEncryption);
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
// they can decrypt with either option
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
// Expected to look like:
// "UserDecryptionOptions": {
// "Object": "userDecryptionOptions"
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "HasAdminApproval": true,
// "HasLoginApprovingDevice": false,
// "HasManageResetPasswordPermission": false
// }
// }
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasAdminApproval", JsonValueKind.False);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasManageResetPasswordPermission", JsonValueKind.False);
// This asserts that device keys are not coming back in the response because this should be a new device.
// if we ever add new properties that come back from here it is fine to change the expected number of properties
// but it should still be asserted in some way that keys are not amongst them.
Assert.Collection(trustedDeviceOption.EnumerateObject(),
p =>
{
Assert.Equal("HasAdminApproval", p.Name);
Assert.Equal(JsonValueKind.False, p.Value.ValueKind);
},
p =>
{
Assert.Equal("HasLoginApprovingDevice", p.Name);
Assert.Equal(JsonValueKind.False, p.Value.ValueKind);
},
p =>
{
Assert.Equal("HasManageResetPasswordPermission", p.Name);
Assert.Equal(JsonValueKind.False, p.Value.ValueKind);
});
}
/// <summary>
/// If a user has a device that is able to accept login with device requests, we should return that state
/// with the user decryption options.
/// </summary>
[Fact]
public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_HasLoginApprovingDevice_ReturnsTrue()
{
using var responseBody = await RunSuccessTestAsync(async factory =>
{
await UpdateUserAsync(factory, user => user.MasterPassword = null);
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
var user = await userRepository.GetByEmailAsync(TestEmail);
var deviceRepository = factory.Services.GetRequiredService<IDeviceRepository>();
await deviceRepository.CreateAsync(new Device
{
Identifier = "my_other_device",
Type = DeviceType.Android,
Name = "Android",
UserId = user.Id,
});
}, MemberDecryptionType.TrustedDeviceEncryption);
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
// they can decrypt with either option
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
// Expected to look like:
// "UserDecryptionOptions": {
// "Object": "userDecryptionOptions"
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "HasAdminApproval": true,
// "HasLoginApprovingDevice": true,
// "HasManageResetPasswordPermission": false
// }
// }
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
// This asserts that device keys are not coming back in the response because this should be a new device.
// if we ever add new properties that come back from here it is fine to change the expected number of properties
// but it should still be asserted in some way that keys are not amongst them.
Assert.Collection(trustedDeviceOption.EnumerateObject(),
p =>
{
Assert.Equal("HasAdminApproval", p.Name);
Assert.Equal(JsonValueKind.False, p.Value.ValueKind);
},
p =>
{
Assert.Equal("HasLoginApprovingDevice", p.Name);
Assert.Equal(JsonValueKind.True, p.Value.ValueKind);
},
p =>
{
Assert.Equal("HasManageResetPasswordPermission", p.Name);
Assert.Equal(JsonValueKind.False, p.Value.ValueKind);
});
}
/// <summary>
/// Story: When a user signs in with SSO on a device they have already signed in with we need to return the keys
/// back to them for the current device if it has been trusted before.
/// </summary>
[Fact]
public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_DeviceAlreadyTrusted_ReturnsOneOption()
{
// Arrange
var challenge = new string('c', 50);
@ -206,13 +260,33 @@ public class IdentityServerSsoTests
await UpdateUserAsync(factory, user => user.MasterPassword = null);
var deviceRepository = factory.Services.GetRequiredService<IDeviceRepository>();
var deviceIdentifier = $"test_id_{Guid.NewGuid()}";
var user = await factory.Services.GetRequiredService<IUserRepository>().GetByEmailAsync(TestEmail);
const string expectedPrivateKey = "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==";
const string expectedUserKey = "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==";
var device = await deviceRepository.CreateAsync(new Device
{
Type = DeviceType.FirefoxBrowser,
Identifier = deviceIdentifier,
Name = "Thing",
UserId = user.Id,
EncryptedPrivateKey = expectedPrivateKey,
EncryptedPublicKey = "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==",
EncryptedUserKey = expectedUserKey,
});
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceIdentifier", deviceIdentifier },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
@ -237,30 +311,43 @@ public class IdentityServerSsoTests
// "Object": "userDecryptionOptions"
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "HasAdminApproval": true
// "HasAdminApproval": true,
// "HasManageResetPasswordPermission": false,
// "EncryptedPrivateKey": "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA==",
// "EncryptedUserKey": "2.QmFzZTY0UGFydA==|QmFzZTY0UGFydA==|QmFzZTY0UGFydA=="
// }
// }
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasAdminApproval", JsonValueKind.False);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasManageResetPasswordPermission", JsonValueKind.False);
var actualPrivateKey = AssertHelper.AssertJsonProperty(trustedDeviceOption, "EncryptedPrivateKey", JsonValueKind.String).GetString();
Assert.Equal(expectedPrivateKey, actualPrivateKey);
var actualUserKey = AssertHelper.AssertJsonProperty(trustedDeviceOption, "EncryptedUserKey", JsonValueKind.String).GetString();
Assert.Equal(expectedUserKey, actualUserKey);
}
// we should add a test case for JIT provisioned users. They don't have any orgs which caused
// an error in the UserHasManageResetPasswordPermission set logic.
/// <summary>
/// Story: When a user with TDE and the manage reset password permission signs in with SSO, we should return
/// TrustedDeviceEncryption.HasManageResetPasswordPermission as true
/// </summary>
[Fact]
public async Task SsoLogin_TrustedDeviceEncryption_FlagTurnedOff_DoesNotReturnOption()
public async Task SsoLogin_TrustedDeviceEncryption_UserHasManageResetPasswordPermission_ReturnsTrue()
{
// Arrange
var challenge = new string('c', 50);
// This creates SsoConfig that HAS enabled trusted device encryption which should have only been
// done with the feature flag turned on but we are testing that even if they have done that, this will turn off
// if returning as an option if the flag has later been turned off. We should be very careful turning the flag
// back off.
// create user permissions with the ManageResetPassword permission
var permissionsWithManageResetPassword = new Permissions() { ManageResetPassword = true };
var factory = await CreateFactoryAsync(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}, challenge, trustedDeviceEnabled: false);
await UpdateUserAsync(factory, user => user.MasterPassword = null);
}, challenge, permissions: permissionsWithManageResetPassword);
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
@ -286,6 +373,33 @@ public class IdentityServerSsoTests
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasAdminApproval", JsonValueKind.False);
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasManageResetPasswordPermission", JsonValueKind.True);
}
[Fact]
public async Task SsoLogin_TrustedDeviceEncryption_FlagTurnedOff_DoesNotReturnOption()
{
// This creates SsoConfig that HAS enabled trusted device encryption which should have only been
// done with the feature flag turned on but we are testing that even if they have done that, this will turn off
// if returning as an option if the flag has later been turned off. We should be very careful turning the flag
// back off.
using var responseBody = await RunSuccessTestAsync(async factory =>
{
await UpdateUserAsync(factory, user => user.MasterPassword = null);
}, MemberDecryptionType.TrustedDeviceEncryption, trustedDeviceEnabled: false);
// Assert
// If the organization has selected TrustedDeviceEncryption but the user still has their master password
// they can decrypt with either option
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
var userDecryptionOptions = AssertHelper.AssertJsonProperty(root, "UserDecryptionOptions", JsonValueKind.Object);
// Expected to look like:
@ -301,36 +415,11 @@ public class IdentityServerSsoTests
[Fact]
public async Task SsoLogin_KeyConnector_ReturnsOptions()
{
// Arrange
var challenge = new string('c', 50);
var factory = await CreateFactoryAsync(new SsoConfigurationData
using var responseBody = await RunSuccessTestAsync(async factory =>
{
MemberDecryptionType = MemberDecryptionType.KeyConnector,
KeyConnectorUrl = "https://key_connector.com"
}, challenge);
await UpdateUserAsync(factory, user => user.MasterPassword = null);
}, MemberDecryptionType.KeyConnector, "https://key_connector.com");
await UpdateUserAsync(factory, user => user.MasterPassword = null);
// Act
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
{ "twoFactorRemember", "0" },
{ "grant_type", "authorization_code" },
{ "code", "test_code" },
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}));
// Assert
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
using var responseBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = responseBody.RootElement;
AssertHelper.AssertJsonProperty(root, "access_token", JsonValueKind.String);
@ -354,7 +443,51 @@ public class IdentityServerSsoTests
Assert.Equal("https://key_connector.com", keyConnectorUrl);
}
private static async Task<IdentityApplicationFactory> CreateFactoryAsync(SsoConfigurationData ssoConfigurationData, string challenge, bool trustedDeviceEnabled = true)
private static async Task<JsonDocument> RunSuccessTestAsync(MemberDecryptionType memberDecryptionType)
{
return await RunSuccessTestAsync(factory => Task.CompletedTask, memberDecryptionType);
}
private static async Task<JsonDocument> RunSuccessTestAsync(Func<IdentityApplicationFactory, Task> configureFactory,
MemberDecryptionType memberDecryptionType,
string? keyConnectorUrl = null,
bool trustedDeviceEnabled = true)
{
var challenge = new string('c', 50);
var factory = await CreateFactoryAsync(new SsoConfigurationData
{
MemberDecryptionType = memberDecryptionType,
KeyConnectorUrl = keyConnectorUrl,
}, challenge, trustedDeviceEnabled);
await configureFactory(factory);
var context = await factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", "10" },
{ "deviceIdentifier", "test_id" },
{ "deviceName", "firefox" },
{ "twoFactorToken", "TEST"},
{ "twoFactorProvider", "5" }, // RememberMe Provider
{ "twoFactorRemember", "0" },
{ "grant_type", "authorization_code" },
{ "code", "test_code" },
{ "code_verifier", challenge },
{ "redirect_uri", "https://localhost:8080/sso-connector.html" }
}));
// Only calls that result in a 200 OK should call this helper
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
return await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
}
private static async Task<IdentityApplicationFactory> CreateFactoryAsync(
SsoConfigurationData ssoConfigurationData,
string challenge,
bool trustedDeviceEnabled = true,
Permissions? permissions = null)
{
var factory = new IdentityApplicationFactory();
@ -367,7 +500,7 @@ public class IdentityServerSsoTests
RedirectUri = "https://localhost:8080/sso-connector.html",
RequestedScopes = new[] { "api", "offline_access" },
CodeChallenge = challenge.Sha256(),
CodeChallengeMethod = "plain", //
CodeChallengeMethod = "plain", //
Subject = null, // Temporarily set it to null
};
@ -400,12 +533,17 @@ public class IdentityServerSsoTests
});
var organizationUserRepository = factory.Services.GetRequiredService<IOrganizationUserRepository>();
var orgUserPermissions =
(permissions == null) ? null : JsonSerializer.Serialize(permissions, JsonHelpers.CamelCase);
var organizationUser = await organizationUserRepository.CreateAsync(new OrganizationUser
{
UserId = user.Id,
OrganizationId = organization.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = OrganizationUserType.User,
Permissions = orgUserPermissions
});
var ssoConfigRepository = factory.Services.GetRequiredService<ISsoConfigRepository>();

View File

@ -274,7 +274,7 @@ public class OrganizationUserRepositoryTests
}
// Act
var result = await orgUserRepos[i].GetByUserIdWithPolicyDetailsAsync(savedUser.Id);
var result = await orgUserRepos[i].GetByUserIdWithPolicyDetailsAsync(savedUser.Id, policy.Type);
results.Add(result.FirstOrDefault());
}