mirror of
https://github.com/bitwarden/server.git
synced 2025-01-23 22:01:28 +01:00
4c77f993e5
* [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem (#3037) * [AC-1423] Add AddonProduct and BitwardenProduct properties to BillingSubscriptionItem - Add a helper method to determine the appropriate addon type based on the subscription items StripeId * [AC-1423] Add helper to StaticStore.cs to find a Plan by StripePlanId * [AC-1423] Use the helper method to set SubscriptionInfo.BitwardenProduct * Add SecretsManagerBilling feature flag to Constants * [AC 1409] Secrets Manager Subscription Stripe Integration (#3019) * Adding the Secret manager to the Plan List * Adding the unit test for the StaticStoreTests class * Fix whitespace formatting * Fix whitespace formatting * Price update * Resolving the PR comments * Resolving PR comments * Fixing the whitespace * only password manager plans are return for now * format whitespace * Resolve the test issue * Fixing the failing test * Refactoring the Plan separation * add a unit test for SingleOrDefault * Fix the whitespace format * Separate the PM and SM plans * Fixing the whitespace * Remove unnecessary directive * Fix imports ordering * Fix imports ordering * Resolve imports ordering * Fixing imports ordering * Fix response model, add MaxProjects * Fix filename * Fix format * Fix: seat price should match annual/monthly * Fix service account annual pricing * Changes for secret manager signup and upgradeplan * Changes for secrets manager signup and upgrade * refactoring the code * Format whitespace * remove unnecessary using directive * Resolve the PR comment on Subscription creation * Resolve PR comment * Add password manager to the error message * Add UseSecretsManager to the event log * Resolve PR comment on plan validation * Resolving pr comments for service account count * Resolving pr comments for service account count * Resolve the pr comments * Remove the store procedure that is no-longer needed * Rename a property properly * Resolving the PR comment * Resolve PR comments * Resolving PR comments * Resolving the Pr comments * Resolving some PR comments * Resolving the PR comments * Resolving the build identity build * Add additional Validation * Resolve the Lint issues * remove unnecessary using directive * Remove the white spaces * Adding unit test for the stripe payment * Remove the incomplete test * Fixing the failing test * Fix the failing test * Fix the fail test on organization service * Fix the failing unit test * Fix the whitespace format * Fix the failing test * Fix the whitespace format * resolve pr comments * Fix the lint message * Resolve the PR comments * resolve pr comments * Resolve pr comments * Resolve the pr comments * remove unused code * Added for sm validation test * Fix the whitespace format issues --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * SM-802: Add SecretsManagerBetaColumn SQL migration and Org table update * SM-802: Run EF Migrations for SecretsManagerBeta * SM-802: Update the two Org procs and View, and move data migration to a separate file * SM-802: Add missing comma to Organization_Create * [AC-1418] Add missing SecretsManagerPlan property to OrganizationResponseModel (#3055) * SM-802: Remove extra GO statement from data migration script * [AC 1460] Update Stripe Configuration (#3070) * change the stripeseat id * change service accountId to align with new product * make all the Id name for consistent * SM-802: Add SecretsManagerBeta to OrganizationResponseModel * SM-802: Move SecretsManagerBeta from OrganizationResponseModel to OrganizationSubscriptionResponseModel. Use sp_refreshview instead of sp_refreshsqlmodule in the migration script. * SM-802: Remove OrganizationUserOrganizationDetailsView.sql changes * [AC 1410] Secrets Manager subscription adjustment back-end changes (#3036) * Create UpgradeSecretsManagerSubscription command --------- Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * SM-802: Remove SecretsManagerBetaColumn migration * SM-802: Add SecretsManagerBetaColumn migration * SM-802: Remove OrganizationUserOrganizationDetailsView update * [AC-1495] Extract UpgradePlanAsync into a command (#3081) * This is a pure lift & shift with no refactors * Only register subscription commands in Api --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> * [AC-1503] Fix Stripe integration on organization upgrade (#3084) * Fix SM parameters not being passed to Stripe * Fix flaky test * Fix error message * [AC-1504] Allow SM max autoscale limits to be disabled (#3085) * [AC-1488] Changed SM Signup and Upgrade paths to set SmServiceAccounts to include the plan BaseServiceAccount (#3086) * [AC-1510] Enable access to Secrets Manager to Organization owner for new Subscription (#3089) * Revert changes to ReferenceEvent code (#3091) * Revert changes to ReferenceEvent code This will be done in AC-1481 * Revert ReferenceEventType change * Move NoopServiceAccountRepository to SM and update namespace * [AC-1462] Add secrets manager service accounts autoscaling commands (#3059) * Adding the Secret manager to the Plan List * Adding the unit test for the StaticStoreTests class * Fix whitespace formatting * Fix whitespace formatting * Price update * Resolving the PR comments * Resolving PR comments * Fixing the whitespace * only password manager plans are return for now * format whitespace * Resolve the test issue * Fixing the failing test * Refactoring the Plan separation * add a unit test for SingleOrDefault * Fix the whitespace format * Separate the PM and SM plans * Fixing the whitespace * Remove unnecessary directive * Fix imports ordering * Fix imports ordering * Resolve imports ordering * Fixing imports ordering * Fix response model, add MaxProjects * Fix filename * Fix format * Fix: seat price should match annual/monthly * Fix service account annual pricing * Changes for secret manager signup and upgradeplan * Changes for secrets manager signup and upgrade * refactoring the code * Format whitespace * remove unnecessary using directive * Changes for subscription Update * Update the seatAdjustment and update * Resolve the PR comment on Subscription creation * Resolve PR comment * Add password manager to the error message * Add UseSecretsManager to the event log * Resolve PR comment on plan validation * Resolving pr comments for service account count * Resolving pr comments for service account count * Resolve the pr comments * Remove the store procedure that is no-longer needed * Add a new class for update subscription * Modify the Update subscription for sm * Add the missing property * Rename a property properly * Resolving the PR comment * Resolve PR comments * Resolving PR comments * Resolving the Pr comments * Resolving some PR comments * Resolving the PR comments * Resolving the build identity build * Add additional Validation * Resolve the Lint issues * remove unnecessary using directive * Remove the white spaces * Adding unit test for the stripe payment * Remove the incomplete test * Fixing the failing test * Fix the failing test * Fix the fail test on organization service * Fix the failing unit test * Fix the whitespace format * Fix the failing test * Fix the whitespace format * resolve pr comments * Fix the lint message * refactor the code * Fix the failing Test * adding a new endpoint * Remove the unwanted code * Changes for Command and Queries * changes for command and queries * Fix the Lint issues * Fix imports ordering * Resolve the PR comments * resolve pr comments * Resolve pr comments * Fix the failing test on adjustSeatscommandtests * Fix the failing test * Fix the whitespaces * resolve failing test * rename a property * Resolve the pr comments * refactoring the existing implementation * Resolve the whitespaces format issue * Resolve the pr comments * [AC-1462] Created IAvailableServiceAccountsQuery along its implementation and with unit tests * [AC-1462] Renamed ICountNewServiceAccountSlotsRequiredQuery * [AC-1462] Added IAutoscaleServiceAccountsCommand and implementation * Add more unit testing * fix the whitespaces issues * [AC-1462] Added unit tests for AutoscaleServiceAccountsCommand * Add more unit test * Remove unnecessary directive * Resolve some pr comments * Adding more unit test * adding more test * add more test * Resolving some pr comments * Resolving some pr comments * Resolving some pr comments * resolve some pr comments * Resolving pr comments * remove whitespaces * remove white spaces * Resolving pr comments * resolving pr comments and fixing white spaces * resolving the lint error * Run dotnet format * resolving the pr comments * Add a missing properties to plan response model * Add the email sender for sm seat and service acct * Add the email sender for sm seat and service acct * Fix the failing test after email sender changes * Add staticstorewrapper to properly test the plans * Add more test and validate the existing test * Fix the white spaces issues * Remove staticstorewrapper and fix the test * fix a null issue on autoscaling * Suggestion: do all seat calculations in update model * Resolve some pr comments * resolving some pr comments * Return value is unnecessary * Resolve the failing test * resolve pr comments * Resolve the pr comments * Resolving admin api failure and adding more test * Resolve the issue failing admin project * Fixing the failed test * Clarify naming and add comments * Clarify naming conventions * Dotnet format * Fix the failing dependency * remove similar test * [AC-1462] Rewrote AutoscaleServiceAccountsCommand to use UpdateSecretsManagerSubscriptionCommand which has the same logic * [AC-1462] Deleted IAutoscaleServiceAccountsCommand as the logic will be moved to UpdateSecretsManagerSubscriptionCommand * [AC-1462] Created method AdjustSecretsManagerServiceAccountsAsync * [AC-1462] Changed SecretsManagerSubscriptionUpdate to only be set by its constructor * [AC-1462] Added check to CountNewServiceAccountSlotsRequiredQuery and revised unit tests * [AC-1462] Revised logic for CountNewServiceAccountSlotsRequiredQuery and fixed unit tests * [AC-1462] Changed SecretsManagerSubscriptionUpdate to receive Organization as a parameter and fixed the unit tests * [AC-1462] Renamed IUpdateSecretsManagerSubscriptionCommand methods UpdateSubscriptionAsync and AdjustServiceAccountsAsync * [AC-1462] Rewrote unit test UpdateSubscriptionAsync_ValidInput_Passes * [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection * [AC-1462] Added parameter names to SecretsManagerSubscriptionUpdateRequestModel * [AC-1462] Updated SecretsManagerSubscriptionUpdate logic to handle null parameters. Revised the unit tests to test null values --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> * Add UsePasswordManager to sync data (#3114) * [AC-1522] Fix service account check on upgrading (#3111) * Resolved the checkmarx issues * [AC-1521] Address checkmarx security feedback (#3124) * Reinstate target attribute but add noopener noreferrer * Update date on migration script * Remove unused constant * Revert "Remove unused constant" This reverts commit4fcb9da4d6
. This is required to make feature flags work on the client * [AC-1458] Add Endpoint And Service Logic for secrets manager to existing subscription (#3087) --------- Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * Remove duplicate migrations from incorrectly resolved merge * [AC-1468] Modified CountNewServiceAccountSlotsRequiredQuery to return zero if organization has SecretsManagerBeta == true (#3112) Co-authored-by: Thomas Rittson <trittson@bitwarden.com> * [Ac 1563] Unable to load billing and subscription related pages for non-enterprise organizations (#3138) * Resolve the failing family plan * resolve issues * Resolve code related pr comments * Resolve test related comments * Resolving or comments * [SM-809] Add service account slot limit check (#3093) * Add service account slot limit check * Add query to DI * [AC-1462] Registered CountNewServiceAccountSlotsRequiredQuery for dependency injection * remove duplicate DI entry * Update unit tests * Remove comment * Code review updates --------- Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Rui Tome <rtome@bitwarden.com> * [AC-1461] Secrets manager seat autoscaling (#3121) * Add autoscaling code to invite user, save user, and bulk enable SM flows * Add tests * Delete command for BulkEnableSecretsManager * circular dependency between OrganizationService and UpdateSecretsManagerSubscriptionCommand - fixed by temporarily duplicating ReplaceAndUpdateCache * Unresolvable dependencies in other services - fixed by temporarily registering noop services and moving around some DI code All should be resolved in PM-1880 * Refactor: improve the update object and use it to adjust values, remove excess interfaces on the command * Handle autoscaling-specific errors --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> * Move bitwarden_license include reference into conditional block * [AC 1526]Show current SM seat and service account usage in Bitwarden Portal (#3142) * changes base on the tickets request * Code refactoring * Removed the unwanted method * Add implementation to the new method * Resolve some pr comments * resolve lint issue * resolve pr comments * add the new noop files * Add new noop file and resolve some pr comments * resolve pr comments * removed unused method * Fix prefill when changing plans or starting trials * [AC-1575] Don't overwrite existing org information when changing plans * [AC-1577] Prefill SM configuration section when starting trial * Clarify property names * Set SM subscription based on current usage * Fix label for Service Accounts field * Prevent subscribing to SM on an invalid plan * Edit comment * Set minimum seat count for SM * Don't use hardcoded plan values * Use plans directly in JS * Refactor to getPlan function * Remove unneeded namespace refs * Add GetPlansHelper to provide some typesafety * Use planType from form instead of model * Add @model specification * Add null coalescing * [AC-108] Updated PolicyService to use IApplicationCacheService to determine if an organization uses policies (cherry picked from commitb98b107c4b
) * [AC-108] Removed checking if Organization is enabled --------- Co-authored-by: Shane Melton <smelton@bitwarden.com> Co-authored-by: Thomas Rittson <trittson@bitwarden.com> Co-authored-by: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Co-authored-by: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Co-authored-by: Colton Hurst <colton@coltonhurst.com> Co-authored-by: cyprain-okeke <cokeke@bitwarden.com> Co-authored-by: Conner Turnbull <cturnbull@bitwarden.com> Co-authored-by: Thomas Avery <43214426+Thomas-Avery@users.noreply.github.com>
584 lines
25 KiB
C#
584 lines
25 KiB
C#
using System.Security.Claims;
|
|
using System.Text.Json;
|
|
using Bit.Core;
|
|
using Bit.Core.Auth.Entities;
|
|
using Bit.Core.Auth.Enums;
|
|
using Bit.Core.Auth.Models.Api.Request.Accounts;
|
|
using Bit.Core.Auth.Models.Data;
|
|
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;
|
|
using Bit.IntegrationTestCommon.Factories;
|
|
using Bit.Test.Common.Helpers;
|
|
using IdentityModel;
|
|
using IdentityServer4.Models;
|
|
using IdentityServer4.Stores;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using NSubstitute;
|
|
using Xunit;
|
|
|
|
#nullable enable
|
|
|
|
namespace Bit.Identity.IntegrationTest.Endpoints;
|
|
|
|
public class IdentityServerSsoTests
|
|
{
|
|
const string TestEmail = "sso_user@email.com";
|
|
|
|
[Fact]
|
|
public async Task Test_MasterPassword_DecryptionType()
|
|
{
|
|
// Arrange
|
|
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
|
|
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": true
|
|
// }
|
|
|
|
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
|
|
|
// One property for the Object and one for master password
|
|
Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SsoLogin_TrustedDeviceEncryption_ReturnsOptions()
|
|
{
|
|
// Arrange
|
|
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
|
|
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": true,
|
|
// "TrustedDeviceOption": {
|
|
// "HasAdminApproval": false
|
|
// }
|
|
// }
|
|
|
|
// Should have master password & one for trusted device with admin approval
|
|
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
|
|
|
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
|
|
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasAdminApproval", JsonValueKind.False);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SsoLogin_TrustedDeviceEncryption_WithAdminResetPolicy_ReturnsOptions()
|
|
{
|
|
// Arrange
|
|
using var responseBody = await RunSuccessTestAsync(async factory =>
|
|
{
|
|
var database = factory.GetDatabaseContext();
|
|
|
|
var organization = await database.Organizations.SingleAsync();
|
|
|
|
var user = await database.Users.SingleAsync(u => u.Email == TestEmail);
|
|
|
|
var organizationUser = await database.OrganizationUsers.SingleAsync(
|
|
ou => ou.OrganizationId == organization.Id && ou.UserId == user.Id);
|
|
|
|
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
|
|
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": true,
|
|
// "TrustedDeviceOption": {
|
|
// "HasAdminApproval": true,
|
|
// "HasManageResetPasswordPermission": false
|
|
// }
|
|
// }
|
|
|
|
// Should have one item for master password & one for trusted device with admin approval
|
|
AssertHelper.AssertJsonProperty(userDecryptionOptions, "HasMasterPassword", JsonValueKind.True);
|
|
|
|
var trustedDeviceOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "TrustedDeviceOption", JsonValueKind.Object);
|
|
AssertHelper.AssertJsonProperty(trustedDeviceOption, "HasAdminApproval", JsonValueKind.True);
|
|
}
|
|
|
|
[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);
|
|
var factory = await CreateFactoryAsync(new SsoConfigurationData
|
|
{
|
|
MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption,
|
|
}, challenge);
|
|
|
|
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", deviceIdentifier },
|
|
{ "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);
|
|
|
|
// Expected to look like:
|
|
// "UserDecryptionOptions": {
|
|
// "Object": "userDecryptionOptions"
|
|
// "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);
|
|
}
|
|
|
|
// 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_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()
|
|
{
|
|
// 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:
|
|
// "UserDecryptionOptions": {
|
|
// "Object": "userDecryptionOptions"
|
|
// "HasMasterPassword": false
|
|
// }
|
|
|
|
// Should only have 2 properties
|
|
Assert.Equal(2, userDecryptionOptions.EnumerateObject().Count());
|
|
}
|
|
|
|
[Fact]
|
|
public async Task SsoLogin_KeyConnector_ReturnsOptions()
|
|
{
|
|
using var responseBody = await RunSuccessTestAsync(async factory =>
|
|
{
|
|
await UpdateUserAsync(factory, user => user.MasterPassword = null);
|
|
}, MemberDecryptionType.KeyConnector, "https://key_connector.com");
|
|
|
|
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"
|
|
// "KeyConnectorOption": {
|
|
// "KeyConnectorUrl": "https://key_connector.com"
|
|
// }
|
|
// }
|
|
|
|
var keyConnectorOption = AssertHelper.AssertJsonProperty(userDecryptionOptions, "KeyConnectorOption", JsonValueKind.Object);
|
|
|
|
var keyConnectorUrl = AssertHelper.AssertJsonProperty(keyConnectorOption, "KeyConnectorUrl", JsonValueKind.String).GetString();
|
|
Assert.Equal("https://key_connector.com", keyConnectorUrl);
|
|
|
|
// For backwards compatibility reasons the url should also be on the root
|
|
keyConnectorUrl = AssertHelper.AssertJsonProperty(root, "KeyConnectorUrl", JsonValueKind.String).GetString();
|
|
Assert.Equal("https://key_connector.com", keyConnectorUrl);
|
|
}
|
|
|
|
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();
|
|
|
|
|
|
var authorizationCode = new AuthorizationCode
|
|
{
|
|
ClientId = "web",
|
|
CreationTime = DateTime.UtcNow,
|
|
Lifetime = (int)TimeSpan.FromMinutes(5).TotalSeconds,
|
|
RedirectUri = "https://localhost:8080/sso-connector.html",
|
|
RequestedScopes = new[] { "api", "offline_access" },
|
|
CodeChallenge = challenge.Sha256(),
|
|
CodeChallengeMethod = "plain", //
|
|
Subject = null, // Temporarily set it to null
|
|
};
|
|
|
|
factory.SubstitueService<IAuthorizationCodeStore>(service =>
|
|
{
|
|
service.GetAuthorizationCodeAsync("test_code")
|
|
.Returns(authorizationCode);
|
|
});
|
|
|
|
factory.SubstitueService<IFeatureService>(service =>
|
|
{
|
|
service.IsEnabled(FeatureFlagKeys.TrustedDeviceEncryption, Arg.Any<ICurrentContext>())
|
|
.Returns(trustedDeviceEnabled);
|
|
});
|
|
|
|
// This starts the server and finalizes services
|
|
var registerResponse = await factory.RegisterAsync(new RegisterRequestModel
|
|
{
|
|
Email = TestEmail,
|
|
MasterPasswordHash = "master_password_hash",
|
|
});
|
|
|
|
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
|
var user = await userRepository.GetByEmailAsync(TestEmail);
|
|
|
|
var organizationRepository = factory.Services.GetRequiredService<IOrganizationRepository>();
|
|
var organization = await organizationRepository.CreateAsync(new Organization
|
|
{
|
|
Name = "Test Org",
|
|
UsePolicies = true
|
|
});
|
|
|
|
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>();
|
|
await ssoConfigRepository.CreateAsync(new SsoConfig
|
|
{
|
|
OrganizationId = organization.Id,
|
|
Enabled = true,
|
|
Data = JsonSerializer.Serialize(ssoConfigurationData, JsonHelpers.CamelCase),
|
|
});
|
|
|
|
var subject = new ClaimsPrincipal(new ClaimsIdentity(new[]
|
|
{
|
|
new Claim(JwtClaimTypes.Subject, user.Id.ToString()), // Get real user id
|
|
new Claim(JwtClaimTypes.Name, TestEmail),
|
|
new Claim(JwtClaimTypes.IdentityProvider, "sso"),
|
|
new Claim("organizationId", organization.Id.ToString()),
|
|
new Claim(JwtClaimTypes.SessionId, "SOMETHING"),
|
|
new Claim(JwtClaimTypes.AuthenticationMethod, "external"),
|
|
new Claim(JwtClaimTypes.AuthenticationTime, DateTime.UtcNow.AddMinutes(-1).ToEpochTime().ToString())
|
|
}, "IdentityServer4", JwtClaimTypes.Name, JwtClaimTypes.Role));
|
|
|
|
authorizationCode.Subject = subject;
|
|
|
|
return factory;
|
|
}
|
|
|
|
private static async Task UpdateUserAsync(IdentityApplicationFactory factory, Action<User> changeUser)
|
|
{
|
|
var userRepository = factory.Services.GetRequiredService<IUserRepository>();
|
|
var user = await userRepository.GetByEmailAsync(TestEmail);
|
|
|
|
changeUser(user);
|
|
|
|
await userRepository.ReplaceAsync(user);
|
|
}
|
|
}
|