Skip to content

Commit

Permalink
Feature/auth/pm 2759/add can reset password to user decryption options (
Browse files Browse the repository at this point in the history
#3113)

* PM-2759 - BaseRequestValidator.cs - CreateUserDecryptionOptionsAsync - Add new hasManageResetPasswordPermission for post SSO redirect logic required on client.

* PM-2759 - Update IdentityServerSsoTests.cs to all pass based on the addition of HasManageResetPasswordPermission to TrustedDeviceUserDecryptionOption

* IdentityServerSsoTests.cs - fix typo in test name:  LoggingApproval --> LoginApproval

* PM1259 - Add test case for verifying that TrustedDeviceOption.hasManageResetPasswordPermission is set properly based on user permission

* dotnet format run
  • Loading branch information
JaredSnider-Bitwarden authored Jul 18, 2023
1 parent ca3406d commit 211326f
Show file tree
Hide file tree
Showing 3 changed files with 113 additions and 9 deletions.
3 changes: 3 additions & 0 deletions src/Core/Auth/Models/Api/Response/UserDecryptionOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,19 @@ public class TrustedDeviceUserDecryptionOption
{
public bool HasAdminApproval { get; }
public bool HasLoginApprovingDevice { get; }
public bool HasManageResetPasswordPermission { get; }
public string? EncryptedPrivateKey { get; }
public string? EncryptedUserKey { get; }

public TrustedDeviceUserDecryptionOption(bool hasAdminApproval,
bool hasLoginApprovingDevice,
bool hasManageResetPasswordPermission,
string? encryptedPrivateKey,
string? encryptedUserKey)
{
HasAdminApproval = hasAdminApproval;
HasLoginApprovingDevice = hasLoginApprovingDevice;
HasManageResetPasswordPermission = hasManageResetPasswordPermission;
EncryptedPrivateKey = encryptedPrivateKey;
EncryptedUserKey = encryptedUserKey;
}
Expand Down
6 changes: 6 additions & 0 deletions src/Identity/IdentityServer/BaseRequestValidator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -624,10 +624,16 @@ private async Task<UserDecryptionOptions> CreateUserDecryptionOptionsAsync(User
.Where(d => d.Identifier != device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type))
.Any();

// Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP
// TDE requires single org so grab first id.
var orgId = CurrentContext.Organizations.First().Id;
var hasManageResetPasswordPermission = await CurrentContext.ManageResetPassword(orgId);

var hasAdminApproval = await PolicyService.AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.ResetPassword);
// TrustedDeviceEncryption only exists for SSO, but if that ever changes this value won't always be true
userDecryptionOption.TrustedDeviceOption = new TrustedDeviceUserDecryptionOption(hasAdminApproval,
hasLoginApprovingDevice,
hasManageResetPasswordPermission,
encryptedPrivateKey,
encryptedUserKey);
}
Expand Down
113 changes: 104 additions & 9 deletions test/Identity.IntegrationTest/Endpoints/IdentityServerSsoTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
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;
Expand Down Expand Up @@ -183,7 +184,8 @@ await policyRepository.CreateAsync(new Policy
// "Object": "userDecryptionOptions"
// "HasMasterPassword": true,
// "TrustedDeviceOption": {
// "HasAdminApproval": true
// "HasAdminApproval": true,
// "HasManageResetPasswordPermission": false
// }
// }

Expand Down Expand Up @@ -238,27 +240,42 @@ public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_ReturnsOne
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "HasAdminApproval": true,
// "HasLoginApprovingDevice": false
// "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("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_HasLoggingApprovingDevice_ReturnsTrue()
public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_HasLoginApprovingDevice_ReturnsTrue()
{
// Arrange
var challenge = new string('c', 50);
Expand Down Expand Up @@ -312,7 +329,8 @@ await deviceRepository.CreateAsync(new Device
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "HasAdminApproval": true,
// "HasLoginApprovingDevice": true
// "HasLoginApprovingDevice": true,
// "HasManageResetPasswordPermission": false
// }
// }

Expand All @@ -322,8 +340,21 @@ await deviceRepository.CreateAsync(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("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>
Expand Down Expand Up @@ -394,20 +425,74 @@ public async Task SsoLogin_TrustedDeviceEncryptionAndNoMasterPassword_DeviceAlre
// "HasMasterPassword": false,
// "TrustedDeviceOption": {
// "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);
}

/// <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_UserHasManageResetPasswordPermission_ReturnsTrue()
{
// Arrange
var challenge = new string('c', 50);

// create user permissions with the ManageResetPassword permission
var permissionsWithManageResetPassword = new Permissions() { ManageResetPassword = true };

var factory = await CreateFactoryAsync(new SsoConfigurationData
{
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
}, challenge, permissions: permissionsWithManageResetPassword);

// 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
// 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);

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()
{
Expand Down Expand Up @@ -517,7 +602,12 @@ public async Task SsoLogin_KeyConnector_ReturnsOptions()
Assert.Equal("https://key_connector.com", keyConnectorUrl);
}

private static async Task<IdentityApplicationFactory> CreateFactoryAsync(SsoConfigurationData ssoConfigurationData, string challenge, bool trustedDeviceEnabled = true)
private static async Task<IdentityApplicationFactory> CreateFactoryAsync(
SsoConfigurationData ssoConfigurationData,
string challenge,
bool trustedDeviceEnabled = true,
Permissions? permissions = null
)
{
var factory = new IdentityApplicationFactory();

Expand Down Expand Up @@ -563,12 +653,17 @@ private static async Task<IdentityApplicationFactory> CreateFactoryAsync(SsoConf
});

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>();
Expand Down

0 comments on commit 211326f

Please sign in to comment.