diff --git a/src/App/Pages/Accounts/LoginPageViewModel.cs b/src/App/Pages/Accounts/LoginPageViewModel.cs index ba04b1e4b..74c9fe701 100644 --- a/src/App/Pages/Accounts/LoginPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginPageViewModel.cs @@ -143,6 +143,7 @@ namespace Bit.App.Pages try { await _deviceActionService.ShowLoadingAsync(AppResources.Loading); + await _stateService.SetPreLoginEmailAsync(Email); await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync(); if (string.IsNullOrWhiteSpace(Email)) { diff --git a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs index 616e3ad40..c6da63df8 100644 --- a/src/App/Pages/Accounts/LoginSsoPageViewModel.cs +++ b/src/App/Pages/Accounts/LoginSsoPageViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Net; using System.Threading.Tasks; using System.Windows.Input; using Bit.App.Abstractions; @@ -27,6 +28,7 @@ namespace Bit.App.Pages private readonly IPlatformUtilsService _platformUtilsService; private readonly IStateService _stateService; private readonly ILogger _logger; + private readonly IOrganizationService _organizationService; private string _orgIdentifier; @@ -42,6 +44,7 @@ namespace Bit.App.Pages _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _stateService = ServiceContainer.Resolve("stateService"); _logger = ServiceContainer.Resolve("logger"); + _organizationService = ServiceContainer.Resolve(); PageTitle = AppResources.Bitwarden; @@ -63,9 +66,25 @@ namespace Bit.App.Pages public async Task InitAsync() { - if (string.IsNullOrWhiteSpace(OrgIdentifier)) + try { - OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync(); + if (await TryClaimedDomainLogin()) + { + return; + } + + if (string.IsNullOrWhiteSpace(OrgIdentifier)) + { + OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync(); + } + } + catch (Exception ex) + { + _logger.Exception(ex); + } + finally + { + await _deviceActionService.HideLoadingAsync(); } } @@ -207,5 +226,37 @@ namespace Bit.App.Pages AppResources.AnErrorHasOccurred); } } + + private async Task TryClaimedDomainLogin() + { + try + { + await _deviceActionService.ShowLoadingAsync(AppResources.Loading); + var userEmail = await _stateService.GetPreLoginEmailAsync(); + var claimedDomainOrgDetails = await _organizationService.GetClaimedOrganizationDomainAsync(userEmail); + await _deviceActionService.HideLoadingAsync(); + + if (claimedDomainOrgDetails == null || !claimedDomainOrgDetails.SsoAvailable) + { + return false; + } + + if (string.IsNullOrEmpty(claimedDomainOrgDetails.OrganizationIdentifier)) + { + await _platformUtilsService.ShowDialogAsync(AppResources.OrganizationSsoIdentifierRequired, AppResources.AnErrorHasOccurred); + return false; + } + + OrgIdentifier = claimedDomainOrgDetails.OrganizationIdentifier; + await LogInAsync(); + return true; + } + catch (Exception ex) + { + HandleException(ex); + } + + return false; + } } } diff --git a/src/App/Resources/AppResources.Designer.cs b/src/App/Resources/AppResources.Designer.cs index 45b51397a..53c085175 100644 --- a/src/App/Resources/AppResources.Designer.cs +++ b/src/App/Resources/AppResources.Designer.cs @@ -4564,6 +4564,15 @@ namespace Bit.App.Resources { } } + /// + /// Looks up a localized string similar to Organization SSO identifier required.. + /// + public static string OrganizationSsoIdentifierRequired { + get { + return ResourceManager.GetString("OrganizationSsoIdentifierRequired", resourceCulture); + } + } + /// /// Looks up a localized string similar to Organization identifier. /// diff --git a/src/App/Resources/AppResources.resx b/src/App/Resources/AppResources.resx index e05cb91ab..9605419ac 100644 --- a/src/App/Resources/AppResources.resx +++ b/src/App/Resources/AppResources.resx @@ -2577,4 +2577,7 @@ Do you want to switch to this account? Weak password identified and found in a data breach. Use a strong and unique password to protect your account. Are you sure you want to use this password? + + Organization SSO identifier required. + diff --git a/src/Core/Abstractions/IApiService.cs b/src/Core/Abstractions/IApiService.cs index adbbef1a1..1d4ee9037 100644 --- a/src/Core/Abstractions/IApiService.cs +++ b/src/Core/Abstractions/IApiService.cs @@ -90,5 +90,6 @@ namespace Bit.Core.Abstractions Task PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest); Task GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config); Task GetKnownDeviceAsync(string email, string deviceIdentifier); + Task GetOrgDomainSsoDetailsAsync(string email); } } diff --git a/src/Core/Abstractions/IOrganizationService.cs b/src/Core/Abstractions/IOrganizationService.cs index 07cab5ac0..fb6c937b7 100644 --- a/src/Core/Abstractions/IOrganizationService.cs +++ b/src/Core/Abstractions/IOrganizationService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; +using Bit.Core.Models.Response; namespace Bit.Core.Abstractions { @@ -12,5 +13,6 @@ namespace Bit.Core.Abstractions Task> GetAllAsync(string userId = null); Task ReplaceAsync(Dictionary organizations); Task ClearAllAsync(string userId); + Task GetClaimedOrganizationDomainAsync(string userEmail); } } diff --git a/src/Core/Abstractions/IStateService.cs b/src/Core/Abstractions/IStateService.cs index 43a3d1107..2cf2251ce 100644 --- a/src/Core/Abstractions/IStateService.cs +++ b/src/Core/Abstractions/IStateService.cs @@ -163,7 +163,7 @@ namespace Bit.Core.Abstractions Task GetLastUserShouldConnectToWatchAsync(); Task SetAvatarColorAsync(string value, string userId = null); Task GetAvatarColorAsync(string userId = null); - - + Task GetPreLoginEmailAsync(); + Task SetPreLoginEmailAsync(string value); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7357ffbfb..40c52e9a5 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -40,6 +40,7 @@ public const string NotificationData = "notificationData"; public const string NotificationDataType = "Type"; public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest"; + public const string PreLoginEmailKey = "preLoginEmailKey"; /// /// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in /// which is used to handle Apple Watch state logic diff --git a/src/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs b/src/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs new file mode 100644 index 000000000..c241d21a1 --- /dev/null +++ b/src/Core/Models/Request/OrganizationSsoDomainDetailsRequest.cs @@ -0,0 +1,9 @@ +using System; +namespace Bit.Core.Models.Request +{ + public class OrganizationDomainSsoDetailsRequest + { + public string Email { get; set; } + } +} + diff --git a/src/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs b/src/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs new file mode 100644 index 000000000..a79640197 --- /dev/null +++ b/src/Core/Models/Response/OrganizationDomainSsoDetailsResponse.cs @@ -0,0 +1,13 @@ +using System; +namespace Bit.Core.Models.Response +{ + public class OrganizationDomainSsoDetailsResponse + { + public bool SsoAvailable { get; set; } + public string DomainName { get; set; } + public string OrganizationIdentifier { get; set; } + public bool SsoRequired { get; set; } + public DateTime? VerifiedDate { get; set; } + } +} + diff --git a/src/Core/Services/ApiService.cs b/src/Core/Services/ApiService.cs index 674960c78..7849951a2 100644 --- a/src/Core/Services/ApiService.cs +++ b/src/Core/Services/ApiService.cs @@ -457,6 +457,11 @@ namespace Bit.Core.Services return SendAsync(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false); } + + public Task GetOrgDomainSsoDetailsAsync(string userEmail) + { + return SendAsync(HttpMethod.Post, $"/organizations/domain/sso/details", new OrganizationDomainSsoDetailsRequest { Email = userEmail }, false, true); + } #endregion #region Organization User APIs diff --git a/src/Core/Services/OrganizationService.cs b/src/Core/Services/OrganizationService.cs index 8f25c8076..566096c1e 100644 --- a/src/Core/Services/OrganizationService.cs +++ b/src/Core/Services/OrganizationService.cs @@ -1,19 +1,27 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Linq; +using System.Net; using System.Threading.Tasks; using Bit.Core.Abstractions; +using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Domain; +using Bit.Core.Models.Request; +using Bit.Core.Models.Response; +using Bit.Core.Utilities; namespace Bit.Core.Services { public class OrganizationService : IOrganizationService { private readonly IStateService _stateService; + private readonly IApiService _apiService; - public OrganizationService(IStateService stateService) + public OrganizationService(IStateService stateService, IApiService apiService) { _stateService = stateService; + _apiService = apiService; } public async Task GetAsync(string id) @@ -51,5 +59,23 @@ namespace Bit.Core.Services { await _stateService.SetOrganizationsAsync(null, userId); } + + public async Task GetClaimedOrganizationDomainAsync(string userEmail) + { + try + { + if (string.IsNullOrEmpty(userEmail)) + { + return null; + } + + return await _apiService.GetOrgDomainSsoDetailsAsync(userEmail); + } + catch (ApiException ex) when (ex.Error?.StatusCode == HttpStatusCode.NotFound) + { + // this is a valid case so there is no need to show an error + return null; + } + } } } diff --git a/src/Core/Services/StateService.cs b/src/Core/Services/StateService.cs index 238d79b06..c97000604 100644 --- a/src/Core/Services/StateService.cs +++ b/src/Core/Services/StateService.cs @@ -1208,6 +1208,18 @@ namespace Bit.Core.Services ))?.Profile?.AvatarColor; } + public async Task GetPreLoginEmailAsync() + { + var options = await GetDefaultStorageOptionsAsync(); + return await GetValueAsync(Constants.PreLoginEmailKey, options); + } + + public async Task SetPreLoginEmailAsync(string value) + { + var options = await GetDefaultStorageOptionsAsync(); + await SetValueAsync(Constants.PreLoginEmailKey, value, options); + } + // Helpers private async Task GetValueAsync(string key, StorageOptions options) diff --git a/src/Core/Utilities/ServiceContainer.cs b/src/Core/Utilities/ServiceContainer.cs index 003432f30..eb09bf58f 100644 --- a/src/Core/Utilities/ServiceContainer.cs +++ b/src/Core/Utilities/ServiceContainer.cs @@ -40,7 +40,7 @@ namespace Bit.Core.Utilities return Task.CompletedTask; }, customUserAgent); var appIdService = new AppIdService(storageService); - var organizationService = new OrganizationService(stateService); + var organizationService = new OrganizationService(stateService, apiService); var settingsService = new SettingsService(stateService); var fileUploadService = new FileUploadService(apiService); var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,