/* * Copyright (c) 2023 Proton AG * * This file is part of ProtonVPN. * * ProtonVPN is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * ProtonVPN is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with ProtonVPN. If not, see . */ using System.Text.RegularExpressions; using CommunityToolkit.Mvvm.ComponentModel; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.Web.WebView2.Core; using ProtonVPN.Client.Core.Bases; using ProtonVPN.Client.Core.Bases.ViewModels; using ProtonVPN.Client.Core.Services.Activation; using ProtonVPN.Client.Logic.Auth.Contracts; using ProtonVPN.Client.Logic.Auth.Contracts.Enums; using ProtonVPN.Client.Logic.Auth.Contracts.Models; using ProtonVPN.Client.Settings.Contracts; using ProtonVPN.Configurations.Contracts; using ProtonVPN.Logging.Contracts.Events.AppLogs; namespace ProtonVPN.Client.UI.Login.Overlays; public partial class SsoLoginOverlayViewModel : OverlayViewModelBase { private readonly IUserAuthenticator _userAuthenticator; private readonly ISettings _settings; private readonly IConfiguration _configuration; private readonly Regex _uriRegex = new(".+\\/sso\\/login#token=(?.+)&uid=(?.+)"); [ObservableProperty] private bool _isLoadingPage; [ObservableProperty] private WebView2? _ssoWebView; private string? _ssoResponseToken; public SsoLoginOverlayViewModel( IMainWindowOverlayActivator overlayActivator, IUserAuthenticator userAuthenticator, ISettings settings, IConfiguration configuration, IViewModelHelper viewModelHelper) : base(overlayActivator, viewModelHelper) { _userAuthenticator = userAuthenticator; _settings = settings; _configuration = configuration; } protected override void OnDeactivated() { base.OnDeactivated(); CleanupWebView(); } private void OnNavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs e) { // Intercept redirection to the account login page and extract the response token from the Uri Match match = _uriRegex.Match(e.Uri); if (match.Success && match.Groups["uid"]?.Value == _settings.UnauthUniqueSessionId) { // Cancel navigation and extract response token e.Cancel = true; _ssoResponseToken = match.Groups["token"]?.Value; OverlayActivator.CloseCurrentOverlay(); } } private void OnNavigationCompleted(object sender, CoreWebView2NavigationCompletedEventArgs e) { IsLoadingPage = false; } public async Task AuthenticateAsync(string ssoChallengeToken) { if (string.IsNullOrEmpty(ssoChallengeToken)) { return AuthResult.Fail(AuthError.SsoAuthFailed); } _ssoResponseToken = null; IsLoadingPage = true; await InitializeWebViewAsync(ssoChallengeToken); await OverlayActivator.ShowOverlayAsync(this); return await _userAuthenticator.CompleteSsoAuthAsync(_ssoResponseToken ?? string.Empty); } private async Task InitializeWebViewAsync(string ssoChallengeToken) { try { CreateWebView(); WebView2? webView = SsoWebView; if (webView is null) { return; } if (webView.CoreWebView2 == null) { CoreWebView2Environment environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, _configuration.WebViewFolder, null); // WinUI3 does not support creating CoreWebView2 with custom environment. Set environment variable instead. Environment.SetEnvironmentVariable("WEBVIEW2_USER_DATA_FOLDER", environment.UserDataFolder); await webView.EnsureCoreWebView2Async(); if (webView.CoreWebView2 is null || SsoWebView != webView) { return; } webView.CoreWebView2.Settings.IsWebMessageEnabled = true; } // Delete cookies to prevent auto authentication after a first successful login. webView.CoreWebView2.CookieManager.DeleteAllCookies(); Uri requestUri = new(new Uri(_configuration.Urls.ApiUrl), $"auth/sso/{ssoChallengeToken}"); CoreWebView2WebResourceRequest request = webView.CoreWebView2.Environment.CreateWebResourceRequest( requestUri.AbsoluteUri, "GET", null, $"x-pm-uid: {_settings.UnauthUniqueSessionId}\r\n" + $"Authorization: Bearer {_settings.UnauthAccessToken}"); webView.CoreWebView2.NavigateWithWebResourceRequest(request); } catch (Exception e) { Logger.Error($"Error occured when trying to navigate to the SSO login page.", e); } } private void CreateWebView() { CleanupWebView(); WebView2 webView = new() { VerticalAlignment = VerticalAlignment.Stretch }; webView.NavigationStarting += OnNavigationStarting; webView.NavigationCompleted += OnNavigationCompleted; SsoWebView = webView; } private void CleanupWebView() { if (SsoWebView is null) { return; } WebView2 webView = SsoWebView; SsoWebView = null; webView.NavigationStarting -= OnNavigationStarting; webView.NavigationCompleted -= OnNavigationCompleted; try { webView.Close(); } catch (Exception ex) { Logger.Error("Failed to close WebView2.", ex); } } }