1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-01 13:43:23 +01:00
bitwarden-server/test/Identity.IntegrationTest/Endpoints/IdentityServerTests.cs
Rui Tomé 4c77f993e5
[AC-108] Enterprise policies are not removed when downgraded to teams (#3168)
* [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 commit 4fcb9da4d6.

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 commit b98b107c4b)

* [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>
2023-08-25 11:22:16 +01:00

659 lines
28 KiB
C#

using System.Text.Json;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Identity.IdentityServer;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Microsoft.AspNetCore.TestHost;
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace Bit.Identity.IntegrationTest.Endpoints;
public class IdentityServerTests : IClassFixture<IdentityApplicationFactory>
{
private const int SecondsInMinute = 60;
private const int MinutesInHour = 60;
private const int SecondsInHour = SecondsInMinute * MinutesInHour;
private readonly IdentityApplicationFactory _factory;
public IdentityServerTests(IdentityApplicationFactory factory)
{
_factory = factory;
ReinitializeDbForTests();
}
[Fact]
public async Task WellKnownEndpoint_Success()
{
var context = await _factory.Server.GetAsync("/.well-known/openid-configuration");
using var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var endpointRoot = body.RootElement;
// WARNING: Edits to this file should NOT just be made to "get the test to work" they should be made when intentional
// changes were made to this endpoint and proper testing will take place to ensure clients are backwards compatible
// or loss of functionality is properly noted.
await using var fs = File.OpenRead("openid-configuration.json");
using var knownConfiguration = await JsonSerializer.DeserializeAsync<JsonDocument>(fs);
var knownConfigurationRoot = knownConfiguration.RootElement;
AssertHelper.AssertEqualJson(endpointRoot, knownConfigurationRoot);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypePassword_Success(string deviceId)
{
var username = "test+tokenpassword@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
});
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail(username));
using var body = await AssertDefaultTokenBodyAsync(context);
var root = body.RootElement;
AssertRefreshTokenExists(root);
AssertHelper.AssertJsonProperty(root, "ForcePasswordReset", JsonValueKind.False);
AssertHelper.AssertJsonProperty(root, "ResetMasterPassword", JsonValueKind.False);
var kdf = AssertHelper.AssertJsonProperty(root, "Kdf", JsonValueKind.Number).GetInt32();
Assert.Equal(0, kdf);
var kdfIterations = AssertHelper.AssertJsonProperty(root, "KdfIterations", JsonValueKind.Number).GetInt32();
Assert.Equal(5000, kdfIterations);
AssertUserDecryptionOptions(root);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails(string deviceId)
{
var username = "test+noauthemailheader@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var context = await PostLoginAsync(_factory.Server, username, deviceId, null);
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_grant", error);
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails(string deviceId)
{
var username = "test+badauthheader@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.Request.Headers.Add("Auth-Email", "bad_value"));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_grant", error);
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails(string deviceId)
{
var username = "test+badauthheader@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var context = await PostLoginAsync(_factory.Server, username, deviceId, context => context.SetAuthEmail("bad_value"));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_grant", error);
AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Manager)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersTrue_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
{
var username = $"{generatedUsername}@example.com";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
}).Server;
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
}));
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Manager)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyDisabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
{
var username = $"{generatedUsername}@example.com";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
}).Server;
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
}));
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: false);
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Manager)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task TokenEndpoint_GrantTypePassword_WithAllUserTypes_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersTrue_Throw(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
{
var username = $"{generatedUsername}@example.com";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "true");
}).Server;
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
}));
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
await AssertRequiredSsoAuthenticationResponseAsync(context);
}
[Theory]
[BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.Admin)]
public async Task TokenEndpoint_GrantTypePassword_WithOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Success(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
{
var username = $"{generatedUsername}@example.com";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
}).Server;
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
}));
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
}
[Theory]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Manager)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task TokenEndpoint_GrantTypePassword_WithNonOwnerOrAdmin_WithSsoPolicyEnabled_WithEnforceSsoPolicyForAllUsersFalse_Throws(OrganizationUserType organizationUserType, Guid organizationId, string deviceId, int generatedUsername)
{
var username = $"{generatedUsername}@example.com";
var server = _factory.WithWebHostBuilder(builder =>
{
builder.UseSetting("globalSettings:sso:enforceSsoPolicyForAllUsers", "false");
}).Server;
await server.PostAsync("/accounts/register", JsonContent.Create(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash"
}));
await CreateOrganizationWithSsoPolicyAsync(organizationId, username, organizationUserType, ssoPolicyEnabled: true);
var context = await PostLoginAsync(server, username, deviceId, context => context.SetAuthEmail(username));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
await AssertRequiredSsoAuthenticationResponseAsync(context);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypeRefreshToken_Success(string deviceId)
{
var username = "test+tokenrefresh@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var (_, refreshToken) = await _factory.TokenFromPasswordAsync(username, "master_password_hash", deviceId);
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "refresh_token" },
{ "client_id", "web" },
{ "refresh_token", refreshToken },
}));
using var body = await AssertDefaultTokenBodyAsync(context);
AssertRefreshTokenExists(body.RootElement);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypeClientCredentials_Success(string deviceId)
{
var username = "test+tokenclientcredentials@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var database = _factory.GetDatabaseContext();
var user = await database.Users
.FirstAsync(u => u.Email == username);
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"user.{user.Id}" },
{ "client_secret", user.ApiKey },
{ "scope", "api" },
{ "DeviceIdentifier", deviceId },
{ "DeviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
{ "DeviceName", "firefox" },
}));
await AssertDefaultTokenBodyAsync(context, "api");
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_Success(Bit.Core.Entities.Organization organization, Bit.Core.Entities.OrganizationApiKey organizationApiKey)
{
var orgRepo = _factory.Services.GetRequiredService<IOrganizationRepository>();
organization.Enabled = true;
organization.UseApi = true;
organization = await orgRepo.CreateAsync(organization);
organizationApiKey.OrganizationId = organization.Id;
organizationApiKey.Type = OrganizationApiKeyType.Default;
var orgApiKeyRepo = _factory.Services.GetRequiredService<IOrganizationApiKeyRepository>();
await orgApiKeyRepo.CreateAsync(organizationApiKey);
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"organization.{organization.Id}" },
{ "client_secret", organizationApiKey.ApiKey },
{ "scope", "api.organization" },
}));
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
await AssertDefaultTokenBodyAsync(context, "api.organization");
}
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_BadOrgId_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", "organization.bad_guid_zz&" },
{ "client_secret", "something" },
{ "scope", "api.organization" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
/// <summary>
/// This test currently does not test any code that is not covered by other tests but
/// it shows that we probably have some dead code in <see cref="ClientStore"/>
/// for installation, organization, and user they split on a <c>'.'</c> but have already checked that at least one
/// <c>'.'</c> exists in the <c>client_id</c> by checking it with <see cref="string.StartsWith(string)"/>
/// I believe that idParts.Length > 1 will ALWAYS return true
/// </summary>
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_NoIdPart_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", "organization." },
{ "client_secret", "something" },
{ "scope", "api.organization" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_OrgDoesNotExist_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"organization.{Guid.NewGuid()}" },
{ "client_secret", "something" },
{ "scope", "api.organization" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_InstallationExists_Succeeds(Bit.Core.Entities.Installation installation)
{
var installationRepo = _factory.Services.GetRequiredService<IInstallationRepository>();
installation = await installationRepo.CreateAsync(installation);
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"installation.{installation.Id}" },
{ "client_secret", installation.Key },
{ "scope", "api.push" },
}));
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
await AssertDefaultTokenBodyAsync(context, "api.push", 24 * SecondsInHour);
}
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_InstallationDoesNotExist_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", $"installation.{Guid.NewGuid()}" },
{ "client_secret", "something" },
{ "scope", "api.push" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_BadInstallationId_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", "organization.bad_guid_zz&" },
{ "client_secret", "something" },
{ "scope", "api.organization" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
/// <inheritdoc cref="TokenEndpoint_GrantTypeClientCredentials_AsOrganization_NoIdPart_Fails"/>
[Fact]
public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_NoIdPart_Fails()
{
var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "grant_type", "client_credentials" },
{ "client_id", "installation." },
{ "client_secret", "something" },
{ "scope", "api.push" },
}));
Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode);
var errorBody = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_client", error);
}
[Theory, BitAutoData]
public async Task TokenEndpoint_TooQuickInOneSecond_BlockRequest(string deviceId)
{
const int AmountInOneSecondAllowed = 10;
// The rule we are testing is 10 requests in 1 second
var username = "test+ratelimiting@email.com";
await _factory.RegisterAsync(new RegisterRequestModel
{
Email = username,
MasterPasswordHash = "master_password_hash",
});
var database = _factory.GetDatabaseContext();
var user = await database.Users
.FirstAsync(u => u.Email == username);
var tasks = new Task<HttpContext>[AmountInOneSecondAllowed + 1];
for (var i = 0; i < AmountInOneSecondAllowed + 1; i++)
{
// Queue all the amount of calls allowed plus 1
tasks[i] = MakeRequest();
}
var responses = (await Task.WhenAll(tasks)).ToList();
var blockResponses = responses.Where(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests);
Assert.True(blockResponses.Count() > 0);
Task<HttpContext> MakeRequest()
{
return _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
{ "deviceIdentifier", deviceId },
{ "deviceName", "firefox" },
{ "grant_type", "password" },
{ "username", username },
{ "password", "master_password_hash" },
}), context => context.SetAuthEmail(username).SetIp("1.1.1.2"));
}
}
private async Task<HttpContext> PostLoginAsync(TestServer server, string username, string deviceId, Action<HttpContext> extraConfiguration)
{
return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary<string, string>
{
{ "scope", "api offline_access" },
{ "client_id", "web" },
{ "deviceType", DeviceTypeAsString(DeviceType.FirefoxBrowser) },
{ "deviceIdentifier", deviceId },
{ "deviceName", "firefox" },
{ "grant_type", "password" },
{ "username", username },
{ "password", "master_password_hash" },
}), extraConfiguration);
}
private async Task CreateOrganizationWithSsoPolicyAsync(Guid organizationId, string username, OrganizationUserType organizationUserType, bool ssoPolicyEnabled)
{
var userRepository = _factory.Services.GetService<IUserRepository>();
var organizationRepository = _factory.Services.GetService<IOrganizationRepository>();
var organizationUserRepository = _factory.Services.GetService<IOrganizationUserRepository>();
var policyRepository = _factory.Services.GetService<IPolicyRepository>();
var organization = new Bit.Core.Entities.Organization { Id = organizationId, Enabled = true, UseSso = ssoPolicyEnabled, UsePolicies = true };
await organizationRepository.CreateAsync(organization);
var user = await userRepository.GetByEmailAsync(username);
var organizationUser = new Bit.Core.Entities.OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
Type = organizationUserType
};
await organizationUserRepository.CreateAsync(organizationUser);
var ssoPolicy = new Bit.Core.Entities.Policy { OrganizationId = organization.Id, Type = PolicyType.RequireSso, Enabled = ssoPolicyEnabled };
await policyRepository.CreateAsync(ssoPolicy);
}
private static string DeviceTypeAsString(DeviceType deviceType)
{
return ((int)deviceType).ToString();
}
private static async Task<JsonDocument> AssertDefaultTokenBodyAsync(HttpContext httpContext, string expectedScope = "api offline_access", int expectedExpiresIn = SecondsInHour * 1)
{
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(httpContext);
var root = body.RootElement;
Assert.Equal(JsonValueKind.Object, root.ValueKind);
AssertAccessTokenExists(root);
AssertExpiresIn(root, expectedExpiresIn);
AssertTokenType(root);
AssertScope(root, expectedScope);
return body;
}
private static void AssertTokenType(JsonElement tokenResponse)
{
var tokenTypeProperty = AssertHelper.AssertJsonProperty(tokenResponse, "token_type", JsonValueKind.String).GetString();
Assert.Equal("Bearer", tokenTypeProperty);
}
private static int AssertExpiresIn(JsonElement tokenResponse, int expectedExpiresIn = 3600)
{
var expiresIn = AssertHelper.AssertJsonProperty(tokenResponse, "expires_in", JsonValueKind.Number).GetInt32();
Assert.Equal(expectedExpiresIn, expiresIn);
return expiresIn;
}
private static string AssertAccessTokenExists(JsonElement tokenResponse)
{
return AssertHelper.AssertJsonProperty(tokenResponse, "access_token", JsonValueKind.String).GetString();
}
private static string AssertRefreshTokenExists(JsonElement tokenResponse)
{
return AssertHelper.AssertJsonProperty(tokenResponse, "refresh_token", JsonValueKind.String).GetString();
}
private static string AssertScopeExists(JsonElement tokenResponse)
{
return AssertHelper.AssertJsonProperty(tokenResponse, "scope", JsonValueKind.String).GetString();
}
private static void AssertScope(JsonElement tokenResponse, string expectedScope)
{
var actualScope = AssertScopeExists(tokenResponse);
Assert.Equal(expectedScope, actualScope);
}
private static async Task AssertRequiredSsoAuthenticationResponseAsync(HttpContext context)
{
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString();
Assert.Equal("invalid_grant", error);
var errorDescription = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString();
Assert.StartsWith("sso authentication", errorDescription.ToLowerInvariant());
}
private static void AssertUserDecryptionOptions(JsonElement tokenResponse)
{
var userDecryptionOptions = AssertHelper.AssertJsonProperty(tokenResponse, "UserDecryptionOptions", JsonValueKind.Object)
.EnumerateObject();
Assert.Collection(userDecryptionOptions,
(prop) => { Assert.Equal("HasMasterPassword", prop.Name); Assert.Equal(JsonValueKind.True, prop.Value.ValueKind); },
(prop) => { Assert.Equal("Object", prop.Name); Assert.Equal("userDecryptionOptions", prop.Value.GetString()); });
}
private void ReinitializeDbForTests()
{
var databaseContext = _factory.GetDatabaseContext();
databaseContext.Policies.RemoveRange(databaseContext.Policies);
databaseContext.OrganizationUsers.RemoveRange(databaseContext.OrganizationUsers);
databaseContext.Organizations.RemoveRange(databaseContext.Organizations);
databaseContext.Users.RemoveRange(databaseContext.Users);
databaseContext.SaveChanges();
}
}