using System; using System.Net; using System.Threading.Tasks; using System.Windows.Input; using Bit.App.Abstractions; using Bit.App.Resources; using Bit.App.Utilities; using Bit.Core.Abstractions; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Domain; using Bit.Core.Utilities; using Xamarin.CommunityToolkit.ObjectModel; using Xamarin.Essentials; namespace Bit.App.Pages { public class LoginSsoPageViewModel : BaseViewModel { private const string REDIRECT_URI = "bitwarden://sso-callback"; private readonly IDeviceActionService _deviceActionService; private readonly IAuthService _authService; private readonly ISyncService _syncService; private readonly IApiService _apiService; private readonly IPasswordGenerationService _passwordGenerationService; private readonly ICryptoFunctionService _cryptoFunctionService; private readonly IPlatformUtilsService _platformUtilsService; private readonly IStateService _stateService; private readonly ILogger _logger; private readonly IOrganizationService _organizationService; private string _orgIdentifier; public LoginSsoPageViewModel() { _deviceActionService = ServiceContainer.Resolve("deviceActionService"); _authService = ServiceContainer.Resolve("authService"); _syncService = ServiceContainer.Resolve("syncService"); _apiService = ServiceContainer.Resolve("apiService"); _passwordGenerationService = ServiceContainer.Resolve("passwordGenerationService"); _cryptoFunctionService = ServiceContainer.Resolve("cryptoFunctionService"); _platformUtilsService = ServiceContainer.Resolve("platformUtilsService"); _stateService = ServiceContainer.Resolve("stateService"); _logger = ServiceContainer.Resolve("logger"); _organizationService = ServiceContainer.Resolve(); PageTitle = AppResources.Bitwarden; LogInCommand = new AsyncCommand(LogInAsync, allowsMultipleExecutions: false); } public string OrgIdentifier { get => _orgIdentifier; set => SetProperty(ref _orgIdentifier, value); } public ICommand LogInCommand { get; } public Action StartTwoFactorAction { get; set; } public Action StartSetPasswordAction { get; set; } public Action SsoAuthSuccessAction { get; set; } public Action CloseAction { get; set; } public Action UpdateTempPasswordAction { get; set; } public async Task InitAsync() { try { if (await TryClaimedDomainLogin()) { return; } if (string.IsNullOrWhiteSpace(OrgIdentifier)) { OrgIdentifier = await _stateService.GetRememberedOrgIdentifierAsync(); } } catch (Exception ex) { _logger.Exception(ex); } finally { await _deviceActionService.HideLoadingAsync(); } } public async Task LogInAsync() { try { if (Connectivity.NetworkAccess == NetworkAccess.None) { await _platformUtilsService.ShowDialogAsync(AppResources.InternetConnectionRequiredMessage, AppResources.InternetConnectionRequiredTitle); return; } if (string.IsNullOrWhiteSpace(OrgIdentifier)) { await _platformUtilsService.ShowDialogAsync( string.Format(AppResources.ValidationFieldRequired, AppResources.OrgIdentifier), AppResources.AnErrorHasOccurred, AppResources.Ok); return; } await _deviceActionService.ShowLoadingAsync(AppResources.LoggingIn); var response = await _apiService.PreValidateSso(OrgIdentifier); if (string.IsNullOrWhiteSpace(response?.Token)) { _logger.Error(response is null ? "Login SSO Error: response is null" : "Login SSO Error: response.Token is null or whitespace"); await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError); return; } var ssoToken = response.Token; var passwordOptions = new PasswordGenerationOptions(true); passwordOptions.Length = 64; var codeVerifier = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions); var codeVerifierHash = await _cryptoFunctionService.HashAsync(codeVerifier, CryptoHashAlgorithm.Sha256); var codeChallenge = CoreHelpers.Base64UrlEncode(codeVerifierHash); var state = await _passwordGenerationService.GeneratePasswordAsync(passwordOptions); var url = _apiService.IdentityBaseUrl + "/connect/authorize?" + "client_id=" + _platformUtilsService.GetClientType().GetString() + "&" + "redirect_uri=" + Uri.EscapeDataString(REDIRECT_URI) + "&" + "response_type=code&scope=api%20offline_access&" + "state=" + state + "&code_challenge=" + codeChallenge + "&" + "code_challenge_method=S256&response_mode=query&" + "domain_hint=" + Uri.EscapeDataString(OrgIdentifier) + "&" + "ssoToken=" + Uri.EscapeDataString(ssoToken); WebAuthenticatorResult authResult = null; authResult = await WebAuthenticator.AuthenticateAsync(new Uri(url), new Uri(REDIRECT_URI)); var code = GetResultCode(authResult, state); if (!string.IsNullOrEmpty(code)) { await LogIn(code, codeVerifier, OrgIdentifier); } else { await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError, AppResources.AnErrorHasOccurred); } } catch (ApiException e) { _logger.Exception(e); await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(e?.Error?.GetSingleMessage() ?? AppResources.LoginSsoError, AppResources.AnErrorHasOccurred); } catch (TaskCanceledException) { // user canceled await _deviceActionService.HideLoadingAsync(); } catch (Exception ex) { _logger.Exception(ex); await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.GenericErrorMessage, AppResources.AnErrorHasOccurred); } } private string GetResultCode(WebAuthenticatorResult authResult, string state) { string code = null; if (authResult != null) { authResult.Properties.TryGetValue("state", out var resultState); if (resultState == state) { authResult.Properties.TryGetValue("code", out var resultCode); code = resultCode; } } return code; } private async Task LogIn(string code, string codeVerifier, string orgId) { try { var response = await _authService.LogInSsoAsync(code, codeVerifier, REDIRECT_URI, orgId); await AppHelpers.ResetInvalidUnlockAttemptsAsync(); await _stateService.SetRememberedOrgIdentifierAsync(OrgIdentifier); await _deviceActionService.HideLoadingAsync(); if (response.TwoFactor) { StartTwoFactorAction?.Invoke(); } else if (response.ResetMasterPassword) { StartSetPasswordAction?.Invoke(); } else if (response.ForcePasswordReset) { UpdateTempPasswordAction?.Invoke(); } else { var task = Task.Run(async () => await _syncService.FullSyncAsync(true)); SsoAuthSuccessAction?.Invoke(); } } catch (Exception e) { await _deviceActionService.HideLoadingAsync(); await _platformUtilsService.ShowDialogAsync(AppResources.LoginSsoError, 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; } } }