diff --git a/bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs b/bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs index 967c77d982..4d6f2a93e4 100644 --- a/bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs +++ b/bitwarden_license/src/Portal/Models/SsoConfigDataViewModel.cs @@ -22,6 +22,8 @@ namespace Bit.Portal.Models Guid organizationId) { ConfigType = configurationData.ConfigType; + UseCryptoAgent = configurationData.UseCryptoAgent; + CryptoAgentUrl = configurationData.CryptoAgentUrl; Authority = configurationData.Authority; ClientId = configurationData.ClientId; ClientSecret = configurationData.ClientSecret; @@ -60,6 +62,10 @@ namespace Bit.Portal.Models [Required] [Display(Name = "ConfigType")] public SsoType ConfigType { get; set; } + [Display(Name = "UseCryptoAgent")] + public bool UseCryptoAgent { get; set; } + [Display(Name = "CryptoAgentUrl")] + public string CryptoAgentUrl { get; set; } // OIDC [Display(Name = "Authority")] @@ -138,6 +144,21 @@ namespace Bit.Portal.Models var i18nService = context.GetService(typeof(II18nService)) as I18nService; var model = context.ObjectInstance as SsoConfigDataViewModel; + if (model.UseCryptoAgent && string.IsNullOrWhiteSpace(model.CryptoAgentUrl)) + { + yield return new ValidationResult( + i18nService.GetLocalizedHtmlString("CryptoAgentUrlValidationError"), + new[] { nameof(model.CryptoAgentUrl) }); + } + + if (!string.IsNullOrWhiteSpace(model.CryptoAgentUrl) && + !Uri.TryCreate(model.CryptoAgentUrl, UriKind.Absolute, out _)) + { + yield return new ValidationResult( + i18nService.GetLocalizedHtmlString("CryptoAgentUrlValidationFormatError"), + new[] { nameof(model.CryptoAgentUrl) }); + } + if (model.ConfigType == SsoType.OpenIdConnect) { if (string.IsNullOrWhiteSpace(model.Authority)) @@ -214,6 +235,8 @@ namespace Bit.Portal.Models return new SsoConfigurationData { ConfigType = ConfigType, + UseCryptoAgent = UseCryptoAgent, + CryptoAgentUrl = CryptoAgentUrl, Authority = Authority, ClientId = ClientId, ClientSecret = ClientSecret, diff --git a/bitwarden_license/src/Portal/Views/Sso/Index.cshtml b/bitwarden_license/src/Portal/Views/Sso/Index.cshtml index 66767123f3..62d63e98d3 100644 --- a/bitwarden_license/src/Portal/Views/Sso/Index.cshtml +++ b/bitwarden_license/src/Portal/Views/Sso/Index.cshtml @@ -58,6 +58,24 @@ + +
+
+

@i18nService.T("CryptoAgentConfig")

+
+
+ + +
+
+
+ + + +
+
+
+
diff --git a/src/Api/Controllers/AccountsController.cs b/src/Api/Controllers/AccountsController.cs index 52ff4b082f..e54e143bdb 100644 --- a/src/Api/Controllers/AccountsController.cs +++ b/src/Api/Controllers/AccountsController.cs @@ -16,9 +16,9 @@ using Microsoft.AspNetCore.Mvc; using System; using System.Collections.Generic; using System.Linq; -using System.Text; using System.Threading.Tasks; using Bit.Core.Enums.Provider; +using System.Text.Json; namespace Bit.Api.Controllers { @@ -37,6 +37,7 @@ namespace Bit.Api.Controllers private readonly IUserService _userService; private readonly ISendRepository _sendRepository; private readonly ISendService _sendService; + private readonly ISsoConfigRepository _ssoConfigRepository; public AccountsController( GlobalSettings globalSettings, @@ -46,11 +47,11 @@ namespace Bit.Api.Controllers IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, IPaymentService paymentService, - ISsoUserRepository ssoUserRepository, IUserRepository userRepository, IUserService userService, ISendRepository sendRepository, - ISendService sendService) + ISendService sendService, + ISsoConfigRepository ssoConfigRepository) { _cipherRepository = cipherRepository; _folderRepository = folderRepository; @@ -63,11 +64,12 @@ namespace Bit.Api.Controllers _userService = userService; _sendRepository = sendRepository; _sendService = sendService; + _ssoConfigRepository = ssoConfigRepository; } [HttpPost("prelogin")] [AllowAnonymous] - public async Task PostPrelogin([FromBody]PreloginRequestModel model) + public async Task PostPrelogin([FromBody] PreloginRequestModel model) { var kdfInformation = await _userRepository.GetKdfInformationByEmailAsync(model.Email); if (kdfInformation == null) @@ -84,7 +86,7 @@ namespace Bit.Api.Controllers [HttpPost("register")] [AllowAnonymous] [CaptchaProtected] - public async Task PostRegister([FromBody]RegisterRequestModel model) + public async Task PostRegister([FromBody] RegisterRequestModel model) { var result = await _userService.RegisterUserAsync(model.ToUser(), model.MasterPasswordHash, model.Token, model.OrganizationUserId); @@ -104,13 +106,13 @@ namespace Bit.Api.Controllers [HttpPost("password-hint")] [AllowAnonymous] - public async Task PostPasswordHint([FromBody]PasswordHintRequestModel model) + public async Task PostPasswordHint([FromBody] PasswordHintRequestModel model) { await _userService.SendMasterPasswordHintAsync(model.Email); } [HttpPost("email-token")] - public async Task PostEmailToken([FromBody]EmailTokenRequestModel model) + public async Task PostEmailToken([FromBody] EmailTokenRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -128,7 +130,7 @@ namespace Bit.Api.Controllers } [HttpPost("email")] - public async Task PostEmail([FromBody]EmailRequestModel model) + public async Task PostEmail([FromBody] EmailRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -166,7 +168,7 @@ namespace Bit.Api.Controllers [HttpPost("verify-email-token")] [AllowAnonymous] - public async Task PostVerifyEmailToken([FromBody]VerifyEmailRequestModel model) + public async Task PostVerifyEmailToken([FromBody] VerifyEmailRequestModel model) { var user = await _userService.GetUserByIdAsync(new Guid(model.UserId)); if (user == null) @@ -189,7 +191,7 @@ namespace Bit.Api.Controllers } [HttpPost("password")] - public async Task PostPassword([FromBody]PasswordRequestModel model) + public async Task PostPassword([FromBody] PasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -214,7 +216,7 @@ namespace Bit.Api.Controllers } [HttpPost("set-password")] - public async Task PostSetPasswordAsync([FromBody]SetPasswordRequestModel model) + public async Task PostSetPasswordAsync([FromBody] SetPasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -222,7 +224,7 @@ namespace Bit.Api.Controllers throw new UnauthorizedAccessException(); } - var result = await _userService.SetPasswordAsync(model.ToUser(user), model.MasterPasswordHash, model.Key, + var result = await _userService.SetPasswordAsync(model.ToUser(user), model.MasterPasswordHash, model.Key, model.OrgIdentifier); if (result.Succeeded) { @@ -238,7 +240,7 @@ namespace Bit.Api.Controllers } [HttpPost("verify-password")] - public async Task PostVerifyPassword([FromBody]VerifyPasswordRequestModel model) + public async Task PostVerifyPassword([FromBody] VerifyPasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -257,7 +259,7 @@ namespace Bit.Api.Controllers } [HttpPost("kdf")] - public async Task PostKdf([FromBody]KdfRequestModel model) + public async Task PostKdf([FromBody] KdfRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -282,7 +284,7 @@ namespace Bit.Api.Controllers } [HttpPost("key")] - public async Task PostKey([FromBody]UpdateKeyRequestModel model) + public async Task PostKey([FromBody] UpdateKeyRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -338,7 +340,7 @@ namespace Bit.Api.Controllers } [HttpPost("security-stamp")] - public async Task PostSecurityStamp([FromBody]SecurityStampRequestModel model) + public async Task PostSecurityStamp([FromBody] SecurityStampRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -394,7 +396,7 @@ namespace Bit.Api.Controllers [HttpPut("profile")] [HttpPost("profile")] - public async Task PutProfile([FromBody]UpdateProfileRequestModel model) + public async Task PutProfile([FromBody] UpdateProfileRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -422,7 +424,7 @@ namespace Bit.Api.Controllers } [HttpPost("keys")] - public async Task PostKeys([FromBody]KeysRequestModel model) + public async Task PostKeys([FromBody] KeysRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -448,7 +450,7 @@ namespace Bit.Api.Controllers [HttpDelete] [HttpPost("delete")] - public async Task Delete([FromBody]DeleteAccountRequestModel model) + public async Task Delete([FromBody] DeleteAccountRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -480,14 +482,14 @@ namespace Bit.Api.Controllers [AllowAnonymous] [HttpPost("delete-recover")] - public async Task PostDeleteRecover([FromBody]DeleteRecoverRequestModel model) + public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model) { await _userService.SendDeleteConfirmationAsync(model.Email); } [HttpPost("delete-recover-token")] [AllowAnonymous] - public async Task PostDeleteRecoverToken([FromBody]VerifyDeleteRecoverRequestModel model) + public async Task PostDeleteRecoverToken([FromBody] VerifyDeleteRecoverRequestModel model) { var user = await _userService.GetUserByIdAsync(new Guid(model.UserId)); if (user == null) @@ -511,7 +513,7 @@ namespace Bit.Api.Controllers } [HttpPost("iap-check")] - public async Task PostIapCheck([FromBody]IapCheckRequestModel model) + public async Task PostIapCheck([FromBody] IapCheckRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -605,7 +607,7 @@ namespace Bit.Api.Controllers [HttpPost("payment")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostPayment([FromBody]PaymentRequestModel model) + public async Task PostPayment([FromBody] PaymentRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -623,7 +625,7 @@ namespace Bit.Api.Controllers [HttpPost("storage")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostStorage([FromBody]StorageRequestModel model) + public async Task PostStorage([FromBody] StorageRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -719,7 +721,7 @@ namespace Bit.Api.Controllers [HttpPut("tax")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task PutTaxInfo([FromBody]TaxInfoUpdateRequestModel model) + public async Task PutTaxInfo([FromBody] TaxInfoUpdateRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -757,7 +759,7 @@ namespace Bit.Api.Controllers } [HttpPost("api-key")] - public async Task ApiKey([FromBody]ApiKeyRequestModel model) + public async Task ApiKey([FromBody] ApiKeyRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -778,7 +780,7 @@ namespace Bit.Api.Controllers } [HttpPost("rotate-api-key")] - public async Task RotateApiKey([FromBody]ApiKeyRequestModel model) + public async Task RotateApiKey([FromBody] ApiKeyRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -798,9 +800,9 @@ namespace Bit.Api.Controllers return response; } } - + [HttpPut("update-temp-password")] - public async Task PutUpdateTempPasswordAsync([FromBody]UpdateTempPasswordRequestModel model) + public async Task PutUpdateTempPasswordAsync([FromBody] UpdateTempPasswordRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) @@ -821,5 +823,34 @@ namespace Bit.Api.Controllers throw new BadRequestException(ModelState); } + + [HttpGet("sso-config")] + public async Task GetSsoConfig([FromQuery] string identifier) + { + if (string.IsNullOrWhiteSpace(identifier)) + { + throw new BadRequestException("Identifier", "Identifier required."); + } + + var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(identifier); + var validConfig = false; + if (ssoConfig != null) + { + // TODO: Make sure this user has rights to access this org's sso config data + validConfig = true; + } + + if (!validConfig) + { + throw new BadRequestException("Identifier", "Invalid identifier."); + } + + var ssoConfigData = JsonSerializer.Deserialize(ssoConfig.Data, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + }); + return new SsoConfigResponseModel(ssoConfigData); + } } } diff --git a/src/Core/Models/Api/Response/SsoConfigResponseModel.cs b/src/Core/Models/Api/Response/SsoConfigResponseModel.cs new file mode 100644 index 0000000000..f27ee8b44e --- /dev/null +++ b/src/Core/Models/Api/Response/SsoConfigResponseModel.cs @@ -0,0 +1,21 @@ +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.Api +{ + public class SsoConfigResponseModel + { + public SsoConfigResponseModel() { } + + public SsoConfigResponseModel(SsoConfigurationData ssoConfigData) + { + if (ssoConfigData == null) + { + return; + } + + CryptoAgentUrl = ssoConfigData.UseCryptoAgent ? ssoConfigData.CryptoAgentUrl : null; + } + + public string CryptoAgentUrl { get; set; } + } +} diff --git a/src/Core/Models/Data/SsoConfigurationData.cs b/src/Core/Models/Data/SsoConfigurationData.cs index 19192c3897..789543fa69 100644 --- a/src/Core/Models/Data/SsoConfigurationData.cs +++ b/src/Core/Models/Data/SsoConfigurationData.cs @@ -14,6 +14,8 @@ namespace Bit.Core.Models.Data private const string _saml2ModulePath = "/saml2"; public SsoType ConfigType { get; set; } + public bool UseCryptoAgent { get; set; } + public string CryptoAgentUrl { get; set; } // OIDC public string Authority { get; set; } diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index c2e6c40520..7499d6955a 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -442,22 +442,22 @@ Request ID - + Redirecting - + You are now being returned to the application. Once complete, you may close this tab. - + If IdP Wants Authn Requests Signed - + Always - + Never - + Welcome to the Bitwarden Business Portal @@ -469,33 +469,33 @@ The IdP public certificate provided does not appear to be a valid certificate, please ensure this is a valid, Base64 encoded PEM or CER format public certificate valid for signing: {0} - + Copy the OIDC callback path to your clipboard - + Copy the OIDC signed out callback path to your clipboard - + Copy the SP Entity Id to your clipboard - + Copy the SAML 2.0 Metadata URL to your clipboard - + View the SAML 2.0 Metadata (opens in a new window) - + Copy the Assertion Consumer Service (ACS) URL to your clipboard - + Redirect A SAML binding type, Redirect - + HTTP POST A SAML binding type, HTTP POST - + Artifact A SAML binding type, Artifact @@ -673,4 +673,19 @@ Require new users to be enrolled automatically - + + Crypto Agent Configuration + + + Crypto Agent URL + + + URL is required when using the Crypto Agent. + + + Crypto Agent URL is not a valid URL. + + + Use Crypto Agent + + \ No newline at end of file