using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Models.Api.Request.Accounts; 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 { 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(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(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); } [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(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(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(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 { { "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 { { "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(); organization.Enabled = true; organization.UseApi = true; organization = await orgRepo.CreateAsync(organization); organizationApiKey.OrganizationId = organization.Id; organizationApiKey.Type = OrganizationApiKeyType.Default; var orgApiKeyRepo = _factory.Services.GetRequiredService(); await orgApiKeyRepo.CreateAsync(organizationApiKey); var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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 { { "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(context); var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString(); Assert.Equal("invalid_client", error); } /// /// 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 /// for installation, organization, and user they split on a '.' but have already checked that at least one /// '.' exists in the client_id by checking it with /// I believe that idParts.Length > 1 will ALWAYS return true /// [Fact] public async Task TokenEndpoint_GrantTypeClientCredentials_AsOrganization_NoIdPart_Fails() { var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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(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 { { "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(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(); installation = await installationRepo.CreateAsync(installation); var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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 { { "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(context); var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString(); Assert.Equal("invalid_client", error); } [Fact] public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_BadInsallationId_Fails() { var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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(context); var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString(); Assert.Equal("invalid_client", error); } /// [Fact] public async Task TokenEndpoint_GrantTypeClientCredentials_AsInstallation_NoIdPart_Fails() { var context = await _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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(context); var error = AssertHelper.AssertJsonProperty(errorBody.RootElement, "error", JsonValueKind.String).GetString(); Assert.Equal("invalid_client", error); } [Theory, BitAutoData] public async Task TokenEndpoint_ToQuickInOneSecond_BlockRequest(string deviceId) { const int AmountInOneSecondAllowed = 5; // 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[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(); Assert.Equal(5, responses.Count(c => c.Response.StatusCode == StatusCodes.Status200OK)); Assert.Equal(1, responses.Count(c => c.Response.StatusCode == StatusCodes.Status429TooManyRequests)); Task MakeRequest() { return _factory.Server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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 PostLoginAsync(TestServer server, string username, string deviceId, Action extraConfiguration) { return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { { "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(); var organizationRepository = _factory.Services.GetService(); var organizationUserRepository = _factory.Services.GetService(); var policyRepository = _factory.Services.GetService(); var organization = new Bit.Core.Entities.Organization { Id = organizationId, Enabled = true, UseSso = ssoPolicyEnabled }; 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 AssertDefaultTokenBodyAsync(HttpContext httpContext, string expectedScope = "api offline_access", int expectedExpiresIn = SecondsInHour * 1) { var body = await AssertHelper.AssertResponseTypeIs(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(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 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(); } }