using Bit.Core.AdminConsole.Entities; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Settings; using Bitwarden.License.Test.Sso.IntegrationTest.Utilities; using Duende.IdentityServer.Models; using Duende.IdentityServer.Services; using Microsoft.AspNetCore.Authentication; using NSubstitute; using AuthenticationSchemes = Bit.Core.AuthenticationSchemes; namespace Bit.Sso.IntegrationTest.Utilities; /// /// Contains the factory and all entities created by for use in integration tests. /// public record SsoTestData( SsoApplicationFactory Factory, Organization? Organization, User? User, OrganizationUser? OrganizationUser, SsoConfig? SsoConfig, SsoUser? SsoUser); /// /// Builder for creating SSO test data with seeded database entities. /// public class SsoTestDataBuilder { /// /// This UserIdentifier is a mock for the UserIdentifier we get from the External Identity Provider. /// private string? _userIdentifier; private Action? _organizationConfig; private Action? _userConfig; private Action? _orgUserConfig; private Action? _ssoConfigConfig; private Action? _ssoUserConfig; private Action? _featureFlagConfig; private bool _includeUser = false; private bool _includeSsoUser = false; private bool _includeOrganizationUser = false; private bool _includeSsoConfig = false; private bool _successfulAuth = true; private bool _withNullEmail = false; private bool _isSelfHosted = false; private bool _includeProviderUserId = true; private bool _useNonExistentOrgInAuth = false; private bool _isNativeClient = false; public SsoTestDataBuilder WithOrganization(Action configure) { _organizationConfig = configure; return this; } public SsoTestDataBuilder WithUser(Action? configure = null) { _includeUser = true; _userConfig = configure; return this; } public SsoTestDataBuilder WithOrganizationUser(Action? configure = null) { _includeOrganizationUser = true; _orgUserConfig = configure; return this; } public SsoTestDataBuilder WithSsoConfig(Action? configure = null) { _includeSsoConfig = true; _ssoConfigConfig = configure; return this; } public SsoTestDataBuilder WithSsoUser(Action? configure = null) { _includeSsoUser = true; _ssoUserConfig = configure; return this; } public SsoTestDataBuilder WithFeatureFlags(Action configure) { _featureFlagConfig = configure; return this; } public SsoTestDataBuilder WithFailedAuthentication() { _successfulAuth = false; return this; } public SsoTestDataBuilder WithNullEmail() { _withNullEmail = true; return this; } public SsoTestDataBuilder WithUserIdentifier(string userIdentifier) { _userIdentifier = userIdentifier; return this; } public SsoTestDataBuilder OmitProviderUserId() { _includeProviderUserId = false; return this; } public SsoTestDataBuilder AsSelfHosted() { _isSelfHosted = true; return this; } /// /// Causes the auth result to use a different (non-existent) organization ID than what is seeded /// in the database. This simulates the "organization not found" scenario. /// public SsoTestDataBuilder WithNonExistentOrganizationInAuth() { _useNonExistentOrgInAuth = true; return this; } /// /// Configures the test to simulate a native client (non-browser) OIDC flow. /// Native clients use custom URI schemes (e.g., "bitwarden://callback") instead of http/https. /// This causes ExternalCallback to return a View with 200 status instead of a redirect. /// public SsoTestDataBuilder AsNativeClient() { _isNativeClient = true; return this; } public async Task BuildAsync() { // Create factory var factory = new SsoApplicationFactory(); // Pre-generate IDs and values needed for auth mock (before accessing Services) var organizationId = Guid.NewGuid(); // Use a different org ID in auth if testing "organization not found" scenario var authOrganizationId = _useNonExistentOrgInAuth ? Guid.NewGuid() : organizationId; var providerUserId = _includeProviderUserId ? Guid.NewGuid().ToString() : ""; var userEmail = _withNullEmail ? null : $"user_{Guid.NewGuid()}@test.com"; var userName = "TestUser"; // 1. Configure mocked authentication service BEFORE accessing Services factory.SubstituteService(authService => { if (_successfulAuth) { authService.AuthenticateAsync( Arg.Any(), AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme) .Returns(MockSuccessfulAuthResult.Build( authOrganizationId, providerUserId, userEmail, userName, acrValue: null, _userIdentifier)); } else { authService.AuthenticateAsync( Arg.Any(), AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme) .Returns(AuthenticateResult.Fail("External authentication error")); } }); // 1.a Configure GlobalSettings for Self-Hosted and seat limit factory.SubstituteService(globalSettings => { globalSettings.SelfHosted.Returns(_isSelfHosted); }); // 1.b configure setting feature flags _featureFlagConfig?.Invoke(factory); // 1.c Configure IIdentityServerInteractionService for native client flow if (_isNativeClient) { factory.SubstituteService(interaction => { // Native clients have redirect URIs that don't start with http/https // e.g., "bitwarden://callback" or "com.bitwarden.app://callback" var authorizationRequest = new AuthorizationRequest { RedirectUri = "bitwarden://sso-callback" }; interaction.GetAuthorizationContextAsync(Arg.Any()) .Returns(authorizationRequest); }); } if (!_successfulAuth) { return new SsoTestData(factory, null!, null!, null!, null!, null!); } // 2. Create Organization with defaults (using pre-generated ID) var organization = new Organization { Id = organizationId, Name = "Test Organization", BillingEmail = "billing@test.com", Plan = "Enterprise", Enabled = true, UseSso = true }; _organizationConfig?.Invoke(organization); var orgRepo = factory.Services.GetRequiredService(); organization = await orgRepo.CreateAsync(organization); // 3. Create User with defaults (using pre-generated values) User? user = null; if (_includeUser) { user = new User { Email = userEmail ?? $"email_{Guid.NewGuid()}@test.dev", Name = userName, ApiKey = Guid.NewGuid().ToString(), SecurityStamp = Guid.NewGuid().ToString() }; _userConfig?.Invoke(user); var userRepo = factory.Services.GetRequiredService(); user = await userRepo.CreateAsync(user); } // 4. Create OrganizationUser linking them OrganizationUser? orgUser = null; if (_includeOrganizationUser) { orgUser = new OrganizationUser { OrganizationId = organization.Id, UserId = user!.Id, Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.User }; _orgUserConfig?.Invoke(orgUser); var orgUserRepo = factory.Services.GetRequiredService(); orgUser = await orgUserRepo.CreateAsync(orgUser); } // 4.a Create many OrganizationUser to test seat count logic if (organization.Seats > 1) { var orgUserRepo = factory.Services.GetRequiredService(); var userRepo = factory.Services.GetRequiredService(); var additionalOrgUsers = new List(); for (var i = 1; i <= organization.Seats; i++) { var additionalUser = new User { Email = $"additional_user_{i}_{Guid.NewGuid()}@test.dev", Name = $"AdditionalUser{i}", ApiKey = Guid.NewGuid().ToString(), SecurityStamp = Guid.NewGuid().ToString() }; var createdAdditionalUser = await userRepo.CreateAsync(additionalUser); var additionalOrgUser = new OrganizationUser { OrganizationId = organization.Id, UserId = createdAdditionalUser.Id, Status = OrganizationUserStatusType.Confirmed, Type = OrganizationUserType.User }; additionalOrgUsers.Add(additionalOrgUser); } await orgUserRepo.CreateManyAsync(additionalOrgUsers); } // 5. Create SsoConfig, if ssoConfigConfig is not null SsoConfig? ssoConfig = null; if (_includeSsoConfig) { ssoConfig = new SsoConfig { OrganizationId = authOrganizationId, Enabled = true }; ssoConfig.SetData(new SsoConfigurationData()); _ssoConfigConfig?.Invoke(ssoConfig); var ssoConfigRepo = factory.Services.GetRequiredService(); ssoConfig = await ssoConfigRepo.CreateAsync(ssoConfig); } // 6. Optionally create SsoUser (using pre-generated providerUserId as ExternalId) SsoUser? ssoUser = null; if (_includeSsoUser) { ssoUser = new SsoUser { OrganizationId = organization.Id, UserId = user!.Id, ExternalId = providerUserId }; _ssoUserConfig?.Invoke(ssoUser); var ssoUserRepo = factory.Services.GetRequiredService(); ssoUser = await ssoUserRepo.CreateAsync(ssoUser); } return new SsoTestData(factory, organization, user, orgUser, ssoConfig, ssoUser); } }