[SG-744] Add claimed domain logic to mobile (#2333)

This commit is contained in:
André Bispo 2023-02-20 14:49:20 +00:00 committed by GitHub
parent b26b9ea41b
commit 3f72d35145
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 140 additions and 7 deletions

View file

@ -143,6 +143,7 @@ namespace Bit.App.Pages
try try
{ {
await _deviceActionService.ShowLoadingAsync(AppResources.Loading); await _deviceActionService.ShowLoadingAsync(AppResources.Loading);
await _stateService.SetPreLoginEmailAsync(Email);
await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync(); await AccountSwitchingOverlayViewModel.RefreshAccountViewsAsync();
if (string.IsNullOrWhiteSpace(Email)) if (string.IsNullOrWhiteSpace(Email))
{ {

View file

@ -1,4 +1,5 @@
using System; using System;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Bit.App.Abstractions; using Bit.App.Abstractions;
@ -27,6 +28,7 @@ namespace Bit.App.Pages
private readonly IPlatformUtilsService _platformUtilsService; private readonly IPlatformUtilsService _platformUtilsService;
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IOrganizationService _organizationService;
private string _orgIdentifier; private string _orgIdentifier;
@ -42,6 +44,7 @@ namespace Bit.App.Pages
_platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService"); _platformUtilsService = ServiceContainer.Resolve<IPlatformUtilsService>("platformUtilsService");
_stateService = ServiceContainer.Resolve<IStateService>("stateService"); _stateService = ServiceContainer.Resolve<IStateService>("stateService");
_logger = ServiceContainer.Resolve<ILogger>("logger"); _logger = ServiceContainer.Resolve<ILogger>("logger");
_organizationService = ServiceContainer.Resolve<IOrganizationService>();
PageTitle = AppResources.Bitwarden; PageTitle = AppResources.Bitwarden;
@ -63,11 +66,27 @@ namespace Bit.App.Pages
public async Task InitAsync() public async Task InitAsync()
{ {
try
{
if (await TryClaimedDomainLogin())
{
return;
}
if (string.IsNullOrWhiteSpace(OrgIdentifier)) if (string.IsNullOrWhiteSpace(OrgIdentifier))
{ {
OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync(); OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync();
} }
} }
catch (Exception ex)
{
_logger.Exception(ex);
}
finally
{
await _deviceActionService.HideLoadingAsync();
}
}
public async Task LogInAsync() public async Task LogInAsync()
{ {
@ -207,5 +226,37 @@ namespace Bit.App.Pages
AppResources.AnErrorHasOccurred); AppResources.AnErrorHasOccurred);
} }
} }
private async Task<bool> 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;
}
} }
} }

View file

@ -4564,6 +4564,15 @@ namespace Bit.App.Resources {
} }
} }
/// <summary>
/// Looks up a localized string similar to Organization SSO identifier required..
/// </summary>
public static string OrganizationSsoIdentifierRequired {
get {
return ResourceManager.GetString("OrganizationSsoIdentifierRequired", resourceCulture);
}
}
/// <summary> /// <summary>
/// Looks up a localized string similar to Organization identifier. /// Looks up a localized string similar to Organization identifier.
/// </summary> /// </summary>

View file

@ -2577,4 +2577,7 @@ Do you want to switch to this account?</value>
<data name="WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription" xml:space="preserve"> <data name="WeakPasswordIdentifiedAndFoundInADataBreachAlertDescription" xml:space="preserve">
<value>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?</value> <value>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?</value>
</data> </data>
<data name="OrganizationSsoIdentifierRequired" xml:space="preserve">
<value>Organization SSO identifier required.</value>
</data>
</root> </root>

View file

@ -90,5 +90,6 @@ namespace Bit.Core.Abstractions
Task<PasswordlessLoginResponse> PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest); Task<PasswordlessLoginResponse> PostCreateRequestAsync(PasswordlessCreateLoginRequest passwordlessCreateLoginRequest);
Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config); Task<string> GetUsernameFromAsync(ForwardedEmailServiceType service, UsernameGeneratorConfig config);
Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier); Task<bool> GetKnownDeviceAsync(string email, string deviceIdentifier);
Task<OrganizationDomainSsoDetailsResponse> GetOrgDomainSsoDetailsAsync(string email);
} }
} }

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Response;
namespace Bit.Core.Abstractions namespace Bit.Core.Abstractions
{ {
@ -12,5 +13,6 @@ namespace Bit.Core.Abstractions
Task<List<Organization>> GetAllAsync(string userId = null); Task<List<Organization>> GetAllAsync(string userId = null);
Task ReplaceAsync(Dictionary<string, OrganizationData> organizations); Task ReplaceAsync(Dictionary<string, OrganizationData> organizations);
Task ClearAllAsync(string userId); Task ClearAllAsync(string userId);
Task<OrganizationDomainSsoDetailsResponse> GetClaimedOrganizationDomainAsync(string userEmail);
} }
} }

View file

@ -163,7 +163,7 @@ namespace Bit.Core.Abstractions
Task<bool> GetLastUserShouldConnectToWatchAsync(); Task<bool> GetLastUserShouldConnectToWatchAsync();
Task SetAvatarColorAsync(string value, string userId = null); Task SetAvatarColorAsync(string value, string userId = null);
Task<string> GetAvatarColorAsync(string userId = null); Task<string> GetAvatarColorAsync(string userId = null);
Task<string> GetPreLoginEmailAsync();
Task SetPreLoginEmailAsync(string value);
} }
} }

View file

@ -40,6 +40,7 @@
public const string NotificationData = "notificationData"; public const string NotificationData = "notificationData";
public const string NotificationDataType = "Type"; public const string NotificationDataType = "Type";
public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest"; public const string PasswordlessLoginRequestKey = "passwordlessLoginRequest";
public const string PreLoginEmailKey = "preLoginEmailKey";
/// <summary> /// <summary>
/// This key is used to store the value of "ShouldConnectToWatch" of the last user that had logged in /// 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 /// which is used to handle Apple Watch state logic

View file

@ -0,0 +1,9 @@
using System;
namespace Bit.Core.Models.Request
{
public class OrganizationDomainSsoDetailsRequest
{
public string Email { get; set; }
}
}

View file

@ -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; }
}
}

View file

@ -457,6 +457,11 @@ namespace Bit.Core.Services
return SendAsync<object, object>(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false); return SendAsync<object, object>(HttpMethod.Post, $"/organizations/{id}/leave", null, true, false);
} }
public Task<OrganizationDomainSsoDetailsResponse> GetOrgDomainSsoDetailsAsync(string userEmail)
{
return SendAsync<OrganizationDomainSsoDetailsRequest, OrganizationDomainSsoDetailsResponse>(HttpMethod.Post, $"/organizations/domain/sso/details", new OrganizationDomainSsoDetailsRequest { Email = userEmail }, false, true);
}
#endregion #endregion
#region Organization User APIs #region Organization User APIs

View file

@ -1,19 +1,27 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Bit.Core.Abstractions; using Bit.Core.Abstractions;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Domain; using Bit.Core.Models.Domain;
using Bit.Core.Models.Request;
using Bit.Core.Models.Response;
using Bit.Core.Utilities;
namespace Bit.Core.Services namespace Bit.Core.Services
{ {
public class OrganizationService : IOrganizationService public class OrganizationService : IOrganizationService
{ {
private readonly IStateService _stateService; private readonly IStateService _stateService;
private readonly IApiService _apiService;
public OrganizationService(IStateService stateService) public OrganizationService(IStateService stateService, IApiService apiService)
{ {
_stateService = stateService; _stateService = stateService;
_apiService = apiService;
} }
public async Task<Organization> GetAsync(string id) public async Task<Organization> GetAsync(string id)
@ -51,5 +59,23 @@ namespace Bit.Core.Services
{ {
await _stateService.SetOrganizationsAsync(null, userId); await _stateService.SetOrganizationsAsync(null, userId);
} }
public async Task<OrganizationDomainSsoDetailsResponse> 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;
}
}
} }
} }

View file

@ -1208,6 +1208,18 @@ namespace Bit.Core.Services
))?.Profile?.AvatarColor; ))?.Profile?.AvatarColor;
} }
public async Task<string> GetPreLoginEmailAsync()
{
var options = await GetDefaultStorageOptionsAsync();
return await GetValueAsync<string>(Constants.PreLoginEmailKey, options);
}
public async Task SetPreLoginEmailAsync(string value)
{
var options = await GetDefaultStorageOptionsAsync();
await SetValueAsync(Constants.PreLoginEmailKey, value, options);
}
// Helpers // Helpers
private async Task<T> GetValueAsync<T>(string key, StorageOptions options) private async Task<T> GetValueAsync<T>(string key, StorageOptions options)

View file

@ -40,7 +40,7 @@ namespace Bit.Core.Utilities
return Task.CompletedTask; return Task.CompletedTask;
}, customUserAgent); }, customUserAgent);
var appIdService = new AppIdService(storageService); var appIdService = new AppIdService(storageService);
var organizationService = new OrganizationService(stateService); var organizationService = new OrganizationService(stateService, apiService);
var settingsService = new SettingsService(stateService); var settingsService = new SettingsService(stateService);
var fileUploadService = new FileUploadService(apiService); var fileUploadService = new FileUploadService(apiService);
var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService, var cipherService = new CipherService(cryptoService, stateService, settingsService, apiService,