1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 23:52:50 -05:00

Add support for Key Connector OTP and account migration (#1663)

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Oscar Hinton
2021-11-09 16:37:32 +01:00
committed by GitHub
parent f6bc35b2d0
commit fd37cb5a12
62 changed files with 3799 additions and 306 deletions

View File

@ -11,6 +11,7 @@
User_FailedLogIn2fa = 1006,
User_ClientExportedVault = 1007,
User_UpdatedTempPassword = 1008,
User_MigratedKeyToKeyConnector = 1009,
Cipher_Created = 1100,
Cipher_Updated = 1101,
@ -54,6 +55,10 @@
Organization_PurgedVault = 1601,
// Organization_ClientExportedVault = 1602,
Organization_VaultAccessed = 1603,
Organization_EnabledSso = 1604,
Organization_DisabledSso = 1605,
Organization_EnabledKeyConnector = 1606,
Organization_DisabledKeyConnector = 1607,
Policy_Updated = 1700,

View File

@ -176,7 +176,7 @@ namespace Bit.Core.IdentityServer
customResponse.Add("TwoFactorToken", token);
}
SetSuccessResult(context, user, claims, customResponse);
await SetSuccessResult(context, user, claims, customResponse);
}
protected async Task BuildTwoFactorResultAsync(User user, Organization organization, T context)
@ -256,7 +256,7 @@ namespace Bit.Core.IdentityServer
protected abstract void SetSsoResult(T context, Dictionary<string, object> customResponse);
protected abstract void SetSuccessResult(T context, User user, List<Claim> claims,
protected abstract Task SetSuccessResult(T context, User user, List<Claim> claims,
Dictionary<string, object> customResponse);
protected abstract void SetErrorResult(T context, Dictionary<string, object> customResponse);

View File

@ -24,6 +24,7 @@ namespace Bit.Core.IdentityServer
{
private UserManager<User> _userManager;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IOrganizationRepository _organizationRepository;
public CustomTokenRequestValidator(
UserManager<User> userManager,
@ -47,6 +48,7 @@ namespace Bit.Core.IdentityServer
{
_userManager = userManager;
_ssoConfigRepository = ssoConfigRepository;
_organizationRepository = organizationRepository;
}
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
@ -58,25 +60,6 @@ namespace Bit.Core.IdentityServer
return;
}
await ValidateAsync(context, context.Result.ValidatedRequest);
if (context.Result.CustomResponse != null)
{
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
var organizationId = organizationClaim?.Value ?? "";
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(new Guid(organizationId));
var ssoConfigData = ssoConfig.GetData();
if (ssoConfigData is { UseCryptoAgent: true } && !string.IsNullOrEmpty(ssoConfigData.CryptoAgentUrl))
{
context.Result.CustomResponse["CryptoAgentUrl"] = ssoConfigData.CryptoAgentUrl;
// Prevent clients redirecting to set-password
// TODO: Figure out if we can move this logic to the clients since this might break older clients
// although we will have issues either way with some clients supporting crypto anent and some not
// suggestion: We should roll out the clients before enabling it server wise
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
}
}
protected async override Task<(User, bool)> ValidateContextAsync(CustomTokenRequestValidationContext context)
@ -87,7 +70,7 @@ namespace Bit.Core.IdentityServer
return (user, user != null);
}
protected override void SetSuccessResult(CustomTokenRequestValidationContext context, User user,
protected override async Task SetSuccessResult(CustomTokenRequestValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse)
{
context.Result.CustomResponse = customResponse;
@ -100,6 +83,40 @@ namespace Bit.Core.IdentityServer
context.Result.ValidatedRequest.ClientClaims.Add(claim);
}
}
if (context.Result.CustomResponse == null || user.MasterPassword != null)
{
return;
}
// KeyConnector responses below
// Apikey login
if (context.Result.ValidatedRequest.GrantType == "client_credentials")
{
if (user.UsesKeyConnector) {
// KeyConnectorUrl is configured in the CLI client, just disable master password reset
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return;
}
// SSO login
var organizationClaim = context.Result.ValidatedRequest.Subject?.FindFirst(c => c.Type == "organizationId");
if (organizationClaim?.Value != null)
{
var organizationId = new Guid(organizationClaim.Value);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organizationId);
var ssoConfigData = ssoConfig.GetData();
if (ssoConfigData is { UseKeyConnector: true } && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl))
{
context.Result.CustomResponse["KeyConnectorUrl"] = ssoConfigData.KeyConnectorUrl;
// Prevent clients redirecting to set-password
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
}
}
protected override void SetTwoFactorResult(CustomTokenRequestValidationContext context,

View File

@ -106,13 +106,14 @@ namespace Bit.Core.IdentityServer
return (user, true);
}
protected override void SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
protected override Task SetSuccessResult(ResourceOwnerPasswordValidationContext context, User user,
List<Claim> claims, Dictionary<string, object> customResponse)
{
context.Result = new GrantValidationResult(user.Id.ToString(), "Application",
identityProvider: "bitwarden",
claims: claims.Count > 0 ? claims : null,
customResponse: customResponse);
return Task.CompletedTask;
}
protected override void SetTwoFactorResult(ResourceOwnerPasswordValidationContext context,

View File

@ -0,0 +1,14 @@
{{#>FullHtmlLayout}}
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
<tr style="margin: 0; box-sizing: border-box;">
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none;" valign="top">
Your email verification code is: <b style="margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">{{Token}}</b>
</td>
</tr>
<tr style="margin: 0; box-sizing: border-box;">
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none;" valign="top">
Use this code to complete the protected action in Bitwarden.
</td>
</tr>
</table>
{{/FullHtmlLayout}}

View File

@ -0,0 +1,5 @@
{{#>BasicTextLayout}}
Your email verification code is: {{Token}}
Use this code to complete the protected action in Bitwarden.
{{/BasicTextLayout}}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class DeleteAccountRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +1,10 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Bit.Core.Models.Api;
namespace Bit.Core.Models.Api
{
public class EmailRequestModel
public class EmailRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]
@ -12,9 +12,6 @@ namespace Bit.Core.Models.Api
public string NewEmail { get; set; }
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
[Required]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }
[Required]
public string Token { get; set; }

View File

@ -1,16 +1,14 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Utilities;
using Bit.Core.Models.Api;
namespace Bit.Core.Models.Api
{
public class EmailTokenRequestModel
public class EmailTokenRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]
[StringLength(256)]
public string NewEmail { get; set; }
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,13 +1,9 @@
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class PasswordRequestModel
public class PasswordRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
[Required]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class SecretVerificationRequestModel : IValidatableObject
{
[StringLength(300)]
public string MasterPasswordHash { get; set; }
public string OTP { get; set; }
public string Secret => !string.IsNullOrEmpty(MasterPasswordHash) ? MasterPasswordHash : OTP;
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (string.IsNullOrEmpty(Secret))
{
yield return new ValidationResult("MasterPasswordHash or OTP must be supplied.");
}
}
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class SecurityStampRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -4,7 +4,7 @@ using Bit.Core.Models.Table;
namespace Bit.Core.Models.Api.Request.Accounts
{
public class SetCryptoAgentKeyRequestModel
public class SetKeyConnectorKeyRequestModel
{
[Required]
public string Key { get; set; }

View File

@ -2,9 +2,9 @@
namespace Bit.Core.Models.Api
{
public class ApiKeyRequestModel
public class VerifyOTPRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
public string OTP { get; set; }
}
}

View File

@ -1,12 +0,0 @@
using System;
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class VerifyPasswordRequestModel
{
[Required]
[StringLength(300)]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class CipherPurgeRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -1,10 +0,0 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Core.Models.Api
{
public class OrganizationDeleteRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
}

View File

@ -8,7 +8,6 @@ using Bit.Core.Sso;
using U2F.Core.Utils;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text.Json;
using System.Text.RegularExpressions;
using Bit.Core.Models.Table;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
@ -40,47 +39,11 @@ namespace Bit.Core.Models.Api
{
public SsoConfigurationDataRequest() {}
public SsoConfigurationDataRequest(SsoConfigurationData configurationData)
{
ConfigType = configurationData.ConfigType;
UseCryptoAgent = configurationData.UseCryptoAgent;
CryptoAgentUrl = configurationData.CryptoAgentUrl;
Authority = configurationData.Authority;
ClientId = configurationData.ClientId;
ClientSecret = configurationData.ClientSecret;
MetadataAddress = configurationData.MetadataAddress;
RedirectBehavior = configurationData.RedirectBehavior;
GetClaimsFromUserInfoEndpoint = configurationData.GetClaimsFromUserInfoEndpoint;
IdpEntityId = configurationData.IdpEntityId;
IdpBindingType = configurationData.IdpBindingType;
IdpSingleSignOnServiceUrl = configurationData.IdpSingleSignOnServiceUrl;
IdpSingleLogoutServiceUrl = configurationData.IdpSingleLogoutServiceUrl;
IdpArtifactResolutionServiceUrl = configurationData.IdpArtifactResolutionServiceUrl;
IdpX509PublicCert = configurationData.IdpX509PublicCert;
IdpOutboundSigningAlgorithm = configurationData.IdpOutboundSigningAlgorithm;
IdpAllowUnsolicitedAuthnResponse = configurationData.IdpAllowUnsolicitedAuthnResponse;
IdpDisableOutboundLogoutRequests = configurationData.IdpDisableOutboundLogoutRequests;
IdpWantAuthnRequestsSigned = configurationData.IdpWantAuthnRequestsSigned;
SpNameIdFormat = configurationData.SpNameIdFormat;
SpOutboundSigningAlgorithm = configurationData.SpOutboundSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
SpSigningBehavior = configurationData.SpSigningBehavior;
SpWantAssertionsSigned = configurationData.SpWantAssertionsSigned;
SpValidateCertificates = configurationData.SpValidateCertificates;
SpMinIncomingSigningAlgorithm = configurationData.SpMinIncomingSigningAlgorithm ?? SamlSigningAlgorithms.Sha256;
AdditionalScopes = configurationData.AdditionalScopes;
AdditionalUserIdClaimTypes = configurationData.AdditionalUserIdClaimTypes;
AdditionalEmailClaimTypes = configurationData.AdditionalEmailClaimTypes;
AdditionalNameClaimTypes = configurationData.AdditionalNameClaimTypes;
AcrValues = configurationData.AcrValues;
ExpectedReturnAcrValue = configurationData.ExpectedReturnAcrValue;
}
[Required]
public SsoType ConfigType { get; set; }
// Crypto Agent
public bool UseCryptoAgent { get; set; }
public string CryptoAgentUrl { get; set; }
public bool UseKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC
public string Authority { get; set; }
@ -215,8 +178,8 @@ namespace Bit.Core.Models.Api
return new SsoConfigurationData
{
ConfigType = ConfigType,
UseCryptoAgent = UseCryptoAgent,
CryptoAgentUrl = CryptoAgentUrl,
UseKeyConnector = UseKeyConnector,
KeyConnectorUrl = KeyConnectorUrl,
Authority = Authority,
ClientId = ClientId,
ClientSecret = ClientSecret,

View File

@ -7,7 +7,7 @@ using System.Linq;
namespace Bit.Core.Models.Api
{
public class UpdateTwoFactorAuthenticatorRequestModel : TwoFactorRequestModel
public class UpdateTwoFactorAuthenticatorRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(50)]
@ -38,7 +38,7 @@ namespace Bit.Core.Models.Api
}
}
public class UpdateTwoFactorDuoRequestModel : TwoFactorRequestModel, IValidatableObject
public class UpdateTwoFactorDuoRequestModel : SecretVerificationRequestModel, IValidatableObject
{
[Required]
[StringLength(50)]
@ -111,7 +111,7 @@ namespace Bit.Core.Models.Api
}
}
public class UpdateTwoFactorYubicoOtpRequestModel : TwoFactorRequestModel, IValidatableObject
public class UpdateTwoFactorYubicoOtpRequestModel : SecretVerificationRequestModel, IValidatableObject
{
public string Key1 { get; set; }
public string Key2 { get; set; }
@ -195,7 +195,7 @@ namespace Bit.Core.Models.Api
}
}
public class TwoFactorEmailRequestModel : TwoFactorRequestModel
public class TwoFactorEmailRequestModel : SecretVerificationRequestModel
{
[Required]
[EmailAddress]
@ -231,13 +231,18 @@ namespace Bit.Core.Models.Api
public string Name { get; set; }
}
public class TwoFactorWebAuthnDeleteRequestModel : TwoFactorRequestModel, IValidatableObject
public class TwoFactorWebAuthnDeleteRequestModel : SecretVerificationRequestModel, IValidatableObject
{
[Required]
public int? Id { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
foreach (var validationResult in Validate(validationContext))
{
yield return validationResult;
}
if (!Id.HasValue || Id < 0 || Id > 5)
{
yield return new ValidationResult("Invalid Key Id", new string[] { nameof(Id) });
@ -252,18 +257,12 @@ namespace Bit.Core.Models.Api
public string Token { get; set; }
}
public class TwoFactorProviderRequestModel : TwoFactorRequestModel
public class TwoFactorProviderRequestModel : SecretVerificationRequestModel
{
[Required]
public TwoFactorProviderType? Type { get; set; }
}
public class TwoFactorRequestModel
{
[Required]
public string MasterPasswordHash { get; set; }
}
public class TwoFactorRecoveryRequestModel : TwoFactorEmailRequestModel
{
[Required]

View File

@ -79,6 +79,8 @@ namespace Bit.Core.Models.Api
Email = organizationUser.Email;
TwoFactorEnabled = twoFactorEnabled;
SsoBound = !string.IsNullOrWhiteSpace(organizationUser.SsoExternalId);
// Prevent reset password when using key connector.
ResetPasswordEnrolled = ResetPasswordEnrolled && !organizationUser.UsesKeyConnector;
}
public string Name { get; set; }

View File

@ -38,6 +38,12 @@ namespace Bit.Core.Models.Api
UserId = organization.UserId?.ToString();
ProviderId = organization.ProviderId?.ToString();
ProviderName = organization.ProviderName;
if (organization.SsoConfig != null)
{
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
UsesKeyConnector = ssoConfigData.UseKeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
}
}
public string Id { get; set; }
@ -68,5 +74,7 @@ namespace Bit.Core.Models.Api
public bool HasPublicAndPrivateKeys { get; set; }
public string ProviderId { get; set; }
public string ProviderName { get; set; }
public bool UsesKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
}
}

View File

@ -31,6 +31,7 @@ namespace Bit.Core.Models.Api
PrivateKey = user.PrivateKey;
SecurityStamp = user.SecurityStamp;
ForcePasswordReset = user.ForcePasswordReset;
UsesKeyConnector = user.UsesKeyConnector;
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
ProviderOrganizations =
@ -49,6 +50,7 @@ namespace Bit.Core.Models.Api
public string PrivateKey { get; set; }
public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }

View File

@ -33,5 +33,6 @@ namespace Bit.Core.Models.Data
public string PrivateKey { get; set; }
public Guid? ProviderId { get; set; }
public string ProviderName { get; set; }
public string SsoConfig { get; set; }
}
}

View File

@ -23,6 +23,7 @@ namespace Bit.Core.Models.Data
public string SsoExternalId { get; set; }
public string Permissions { get; set; }
public string ResetPasswordKey { get; set; }
public bool UsesKeyConnector { get; set; }
public Dictionary<TwoFactorProviderType, TwoFactorProvider> GetTwoFactorProviders()
{

View File

@ -1,8 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Sso;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
namespace Bit.Core.Models.Data
@ -13,11 +15,20 @@ namespace Bit.Core.Models.Data
private const string _oidcSignedOutPath = "/oidc-signedout";
private const string _saml2ModulePath = "/saml2";
public static SsoConfigurationData Deserialize(string data)
{
return CoreHelpers.LoadClassFromJsonData<SsoConfigurationData>(data);
}
public string Serialize()
{
return CoreHelpers.ClassToJsonData(this);
}
public SsoType ConfigType { get; set; }
// Crypto Agent
public bool UseCryptoAgent { get; set; }
public string CryptoAgentUrl { get; set; }
public bool UseKeyConnector { get; set; }
public string KeyConnectorUrl { get; set; }
// OIDC
public string Authority { get; set; }

View File

@ -1,5 +1,4 @@
using System;
using System.Text.Json;
using Bit.Core.Models.Data;
namespace Bit.Core.Models.Table
@ -13,11 +12,6 @@ namespace Bit.Core.Models.Table
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
private JsonSerializerOptions _jsonSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
public void SetNewId()
{
// int will be auto-populated
@ -26,12 +20,12 @@ namespace Bit.Core.Models.Table
public SsoConfigurationData GetData()
{
return JsonSerializer.Deserialize<SsoConfigurationData>(Data, _jsonSerializerOptions);
return SsoConfigurationData.Deserialize(Data);
}
public void SetData(SsoConfigurationData data)
{
Data = JsonSerializer.Serialize(data, _jsonSerializerOptions);
Data = data.Serialize();
}
}
}

View File

@ -58,7 +58,7 @@ namespace Bit.Core.Models.Table
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; internal set; } = DateTime.UtcNow;
public bool ForcePasswordReset { get; set; }
public bool UsesCryptoAgent { get; set; }
public bool UsesKeyConnector { get; set; }
public void SetNewId()
{

View File

@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Linq;
using Bit.Core.Models.Data;
using Microsoft.Azure.Documents.SystemFunctions;
namespace Bit.Core.Repositories.EntityFramework.Queries
{
@ -16,8 +17,10 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
from po in po_g.DefaultIfEmpty()
join p in dbContext.Providers on po.ProviderId equals p.Id into p_g
from p in p_g.DefaultIfEmpty()
join ss in dbContext.SsoConfigs on ou.OrganizationId equals ss.OrganizationId into ss_g
from ss in ss_g.DefaultIfEmpty()
where ((su == null || !su.OrganizationId.HasValue) || su.OrganizationId == ou.OrganizationId)
select new { ou, o, su, p };
select new { ou, o, su, p, ss };
return query.Select(x => new OrganizationUserOrganizationDetails
{
OrganizationId = x.ou.OrganizationId,
@ -48,6 +51,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
PrivateKey = x.o.PrivateKey,
ProviderId = x.p.Id,
ProviderName = x.p.Name,
SsoConfig = x.ss.Data,
});
}
}

View File

@ -29,6 +29,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries
SsoExternalId = x.su.ExternalId,
Permissions = x.ou.Permissions,
ResetPasswordKey = x.ou.ResetPasswordKey,
UsesKeyConnector = x.u.UsesKeyConnector,
});
}
}

View File

@ -49,5 +49,6 @@ namespace Bit.Core.Services
Task SendProviderConfirmedEmailAsync(string providerName, string email);
Task SendProviderUserRemoved(string providerName, string email);
Task SendUpdatedTempPasswordEmailAsync(string email, string userName);
Task SendOTPEmailAsync(string email, string token);
}
}

View File

@ -34,7 +34,8 @@ namespace Bit.Core.Services
string token, string key);
Task<IdentityResult> ChangePasswordAsync(User user, string masterPassword, string newMasterPassword, string key);
Task<IdentityResult> SetPasswordAsync(User user, string newMasterPassword, string key, string orgIdentifier = null);
Task<IdentityResult> SetCryptoAgentKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier);
Task<IdentityResult> ConvertToKeyConnectorAsync(User user);
Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType type, Guid orgId, Guid id, string newMasterPassword, string key);
Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint);
Task<IdentityResult> ChangeKdfAsync(User user, string masterPassword, string newMasterPassword, string key,
@ -74,5 +75,8 @@ namespace Bit.Core.Services
Task<string> GenerateSignInTokenAsync(User user, string purpose);
Task RotateApiKeyAsync(User user);
string GetUserName(ClaimsPrincipal principal);
Task SendOTPAsync(User user);
Task<bool> VerifyOTPAsync(User user, string token);
Task<bool> VerifySecretAsync(User user, string secret);
}
}

View File

@ -755,5 +755,20 @@ namespace Bit.Core.Services
message.Category = "UpdatedTempPassword";
await _mailDeliveryService.SendEmailAsync(message);
}
public async Task SendOTPEmailAsync(string email, string token)
{
var message = CreateDefaultMessage("Your Bitwarden Verification Code", email);
var model = new EmailTokenViewModel
{
Token = token,
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
SiteName = _globalSettings.SiteName,
};
await AddMessageContentAsync(message, "OTPEmail", model);
message.MetaData.Add("SendGridBypassListManagement", true);
message.Category = "OTP";
await _mailDeliveryService.SendEmailAsync(message);
}
}
}

View File

@ -908,6 +908,8 @@ namespace Bit.Core.Services
public async Task DeleteAsync(Organization organization)
{
await ValidateDeleteOrganizationAsync(organization);
if (!string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
try
@ -2135,5 +2137,14 @@ namespace Bit.Core.Services
throw new BadRequestException("Custom users can not manage Admins or Owners.");
}
}
private async Task ValidateDeleteOrganizationAsync(Organization organization)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id);
if (ssoConfig?.GetData()?.UseKeyConnector == true)
{
throw new BadRequestException("You cannot delete an Organization that is using Key Connector.");
}
}
}
}

View File

@ -15,6 +15,7 @@ namespace Bit.Core.Services
private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IMailService _mailService;
public PolicyService(
@ -22,12 +23,14 @@ namespace Bit.Core.Services
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IPolicyRepository policyRepository,
ISsoConfigRepository ssoConfigRepository,
IMailService mailService)
{
_eventService = eventService;
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
_policyRepository = policyRepository;
_ssoConfigRepository = ssoConfigRepository;
_mailService = mailService;
}
@ -64,6 +67,12 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Maximum Vault Timeout policy is enabled.");
}
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(org.Id);
if (ssoConfig?.GetData()?.UseKeyConnector == true)
{
throw new BadRequestException("KeyConnector is enabled.");
}
}
break;

View File

@ -1,5 +1,7 @@
using System;
using System.Threading.Tasks;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
@ -8,11 +10,20 @@ namespace Bit.Core.Services
public class SsoConfigService : ISsoConfigService
{
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IPolicyRepository _policyRepository;
private readonly IOrganizationRepository _organizationRepository;
private readonly IEventService _eventService;
public SsoConfigService(
ISsoConfigRepository ssoConfigRepository)
ISsoConfigRepository ssoConfigRepository,
IPolicyRepository policyRepository,
IOrganizationRepository organizationRepository,
IEventService eventService)
{
_ssoConfigRepository = ssoConfigRepository;
_policyRepository = policyRepository;
_organizationRepository = organizationRepository;
_eventService = eventService;
}
public async Task SaveAsync(SsoConfig config)
@ -23,7 +34,49 @@ namespace Bit.Core.Services
{
config.CreationDate = now;
}
var useKeyConnector = config.GetData().UseKeyConnector;
if (useKeyConnector)
{
await VerifyDependenciesAsync(config);
}
var oldConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(config.OrganizationId);
if (oldConfig?.GetData()?.UseKeyConnector == true && !useKeyConnector)
{
throw new BadRequestException("KeyConnector cannot be disabled at this moment.");
}
await LogEventsAsync(config, oldConfig);
await _ssoConfigRepository.UpsertAsync(config);
}
private async Task VerifyDependenciesAsync(SsoConfig config)
{
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.SingleOrg);
if (policy is not { Enabled: true })
{
throw new BadRequestException("KeyConnector requires Single Organization to be enabled.");
}
}
private async Task LogEventsAsync(SsoConfig config, SsoConfig oldConfig)
{
var organization = await _organizationRepository.GetByIdAsync(config.OrganizationId);
if (oldConfig?.Enabled != config.Enabled)
{
var e = config.Enabled ? EventType.Organization_EnabledSso : EventType.Organization_DisabledSso;
await _eventService.LogOrganizationEventAsync(organization, e);
}
var useKeyConnector = config.GetData().UseKeyConnector;
if (oldConfig?.GetData()?.UseKeyConnector != useKeyConnector)
{
var e = useKeyConnector
? EventType.Organization_EnabledKeyConnector
: EventType.Organization_DisabledKeyConnector;
await _eventService.LogOrganizationEventAsync(organization, e);
}
}
}
}

View File

@ -1,26 +1,27 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Fido2NetLib;
using Fido2NetLib.Objects;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Bit.Core.Models.Table;
using Bit.Core.Repositories;
using System.Linq;
using Bit.Core.Enums;
using System.Security.Claims;
using Bit.Core.Models;
using Bit.Core.Models.Business;
using U2fLib = U2F.Core.Crypto.U2F;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Utilities;
using Bit.Core.Settings;
using System.IO;
using Newtonsoft.Json;
using Microsoft.AspNetCore.DataProtection;
using Fido2NetLib;
using Fido2NetLib.Objects;
using File = System.IO.File;
using U2fLib = U2F.Core.Crypto.U2F;
namespace Bit.Core.Services
{
@ -602,7 +603,7 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
public async Task<IdentityResult> SetPasswordAsync(User user, string masterPassword, string key,
string orgIdentifier = null)
{
if (user == null)
@ -627,42 +628,63 @@ namespace Bit.Core.Services
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
if (!string.IsNullOrWhiteSpace(orgIdentifier))
{
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
}
return IdentityResult.Success;
}
public async Task<IdentityResult> SetCryptoAgentKeyAsync(User user, string key, string orgIdentifier)
public async Task<IdentityResult> SetKeyConnectorKeyAsync(User user, string key, string orgIdentifier)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UsesCryptoAgent)
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses crypto agent.");
Logger.LogWarning("Already uses key connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.Key = key;
user.UsesCryptoAgent = true;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
// TODO: Use correct event
await _eventService.LogUserEventAsync(user.Id, EventType.User_ChangedPassword);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
await _organizationService.AcceptUserAsync(orgIdentifier, user, this);
return IdentityResult.Success;
}
public async Task<IdentityResult> ConvertToKeyConnectorAsync(User user)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (user.UsesKeyConnector)
{
Logger.LogWarning("Already uses key connector.");
return IdentityResult.Failed(_identityErrorDescriber.UserAlreadyHasPassword());
}
user.RevisionDate = user.AccountRevisionDate = DateTime.UtcNow;
user.MasterPassword = null;
user.UsesKeyConnector = true;
await _userRepository.ReplaceAsync(user);
await _eventService.LogUserEventAsync(user.Id, EventType.User_MigratedKeyToKeyConnector);
return IdentityResult.Success;
}
public async Task<IdentityResult> AdminResetPasswordAsync(OrganizationUserType callingUserType, Guid orgId, Guid id, string newMasterPassword, string key)
{
// Org must be able to use reset password
@ -671,15 +693,15 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Organization does not allow password reset.");
}
// Enterprise policy must be enabled
// Enterprise policy must be enabled
var resetPasswordPolicy =
await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
if (resetPasswordPolicy == null || !resetPasswordPolicy.Enabled)
{
throw new BadRequestException("Organization does not have the password reset policy enabled.");
}
// Org User must be confirmed and have a ResetPasswordKey
var orgUser = await _organizationUserRepository.GetByIdAsync(id);
if (orgUser == null || orgUser.Status != OrganizationUserStatusType.Confirmed ||
@ -688,7 +710,7 @@ namespace Bit.Core.Services
{
throw new BadRequestException("Organization User not valid");
}
// Calling User must be of higher/equal user type to reset user's password
var canAdjustPassword = false;
switch (callingUserType)
@ -715,7 +737,12 @@ namespace Bit.Core.Services
{
throw new NotFoundException();
}
if (user.UsesKeyConnector)
{
throw new BadRequestException("Cannot reset password of a user with key connector.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
@ -733,14 +760,14 @@ namespace Bit.Core.Services
return IdentityResult.Success;
}
public async Task<IdentityResult> UpdateTempPasswordAsync(User user, string newMasterPassword, string key, string hint)
{
if (!user.ForcePasswordReset)
{
throw new BadRequestException("User does not have a temporary password to update.");
}
var result = await UpdatePasswordHash(user, newMasterPassword);
if (!result.Succeeded)
{
@ -820,14 +847,14 @@ namespace Bit.Core.Services
return IdentityResult.Failed(_identityErrorDescriber.PasswordMismatch());
}
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPassword)
public async Task<IdentityResult> RefreshSecurityStampAsync(User user, string secret)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
if (await CheckPasswordAsync(user, masterPassword))
if (await VerifySecretAsync(user, secret))
{
var result = await base.UpdateSecurityStampAsync(user);
if (!result.Succeeded)
@ -878,7 +905,7 @@ namespace Bit.Core.Services
}
}
public async Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode,
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode,
IOrganizationService organizationService)
{
var user = await _userRepository.GetByEmailAsync(email);
@ -888,7 +915,7 @@ namespace Bit.Core.Services
return false;
}
if (!await CheckPasswordAsync(user, masterPassword))
if (!await VerifySecretAsync(user, secret))
{
return false;
}
@ -1253,7 +1280,7 @@ namespace Bit.Core.Services
purpose);
return token;
}
private async Task<IdentityResult> UpdatePasswordHash(User user, string newPassword,
bool validatePassword = true, bool refreshStamp = true)
{
@ -1350,5 +1377,35 @@ namespace Bit.Core.Services
user.RevisionDate = DateTime.UtcNow;
await _userRepository.ReplaceAsync(user);
}
public async Task SendOTPAsync(User user)
{
if (user.Email == null)
{
throw new BadRequestException("No user email.");
}
if (!user.UsesKeyConnector)
{
throw new BadRequestException("Not using key connector.");
}
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email);
await _mailService.SendOTPEmailAsync(user.Email, token);
}
public Task<bool> VerifyOTPAsync(User user, string token)
{
return base.VerifyUserTokenAsync(user, TokenOptions.DefaultEmailProvider,
"otp:" + user.Email, token);
}
public async Task<bool> VerifySecretAsync(User user, string secret)
{
return user.UsesKeyConnector
? await VerifyOTPAsync(user, secret)
: await CheckPasswordAsync(user, secret);
}
}
}

View File

@ -200,5 +200,10 @@ namespace Bit.Core.Services
{
return Task.FromResult(0);
}
public Task SendOTPEmailAsync(string email, string token)
{
return Task.FromResult(0);
}
}
}

View File

@ -895,6 +895,16 @@ namespace Bit.Core.Utilities
return System.Text.Json.JsonSerializer.Deserialize<T>(jsonData, options);
}
public static string ClassToJsonData<T>(T data)
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
return System.Text.Json.JsonSerializer.Serialize(data, options);
}
public static ICollection<T> AddIfNotExists<T>(this ICollection<T> list, T item)
{
if (list.Contains(item))

View File

@ -175,7 +175,7 @@ namespace Bit.Core.Utilities
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddSingleton<IDeviceService, DeviceService>();
services.AddSingleton<IAppleIapService, AppleIapService>();
services.AddSingleton<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISsoConfigService, SsoConfigService>();
services.AddScoped<ISendService, SendService>();
}