CVE-2020-15679: Session Fixation (CWE-384)
value: An OAuth session fixation vulnerability existed in the VPN login flow, where an attacker could craft a custom login URL, convince a VPN user to login via that URL, and obtain authenticated access as that user. This issue is limited to cases where attacker and victim are sharing the same source IP and could allow the ability to view session states and disconnect VPN sessions. This vulnerability affects Mozilla VPN iOS 1.0.7 < (929), Mozilla VPN Windows < 1.2.2, and Mozilla VPN Android 1.1.0 < (1360).
CWE description: Authenticating a user, or otherwise establishing a new user session, without invalidating any existing session identifier gives an attacker the opportunity to steal authenticated sessions.
CVSS v3 base score: 7.6
Published: 2022-12-22T20:15Z
Repository: https://github.com/mozilla-services/guardian-vpn-windows
Language: C#
Files changed: installer/MozillaVPN.wxs, ui/src/Firefox Private Network.csproj	, ui/src/FxA/Account.cs, ui/src/FxA/Devices.cs, ui/src/FxA/Login.cs, ui/src/FxA/LoginSessionManager.cs, ui/src/FxA/ServerList/ServerList.cs, ui/src/JSONStructures/Login/FxALoginURLs.cs, ui/src/Main.cs, ui/src/Manager.cs, ui/src/ProductConstants.cs, ui/src/UI/Components/Card/Hero/HeroSubtext.xaml, ui/src/UI/Resources/Localization/Translations/en-us.ftl, ui/src/UI/Views/Settings/SettingsView.xaml, ui/src/ViewModels/MainWindowViewModel.cs, ui/src/WCF/IService.cs, ui/src/WCF/Service.cs, ui/src/app.manifest, ui/src/packages.config
Commit date: 2020-10-23 10:07:14 -0400
Commit message: New non-polling authentication flow.

- Fixes intermittent hanging during auth
- Implements PKCE

Diff: diff --git a/installer/MozillaVPN.wxs b/installer/MozillaVPN.wxs
index 49a02e4..be6a8be 100644
--- a/installer/MozillaVPN.wxs
+++ b/installer/MozillaVPN.wxs
@@ -84,10 +84,22 @@
 
 				<File Source="../ui/bin/$(var.Platform)/Release/MozillaVPN.visualelementsmanifest.xml" />
 				<File Source="../ui/bin/$(var.Platform)/Release/Resources/Graphics/icon_70x70.png" />
 				<File Source="../ui/bin/$(var.Platform)/Release/Resources/Graphics/icon_150x150.png" />
 
+        <!-- Custom URL protocol for handling the PKCE auth flow -->
+        <RegistryKey Root="HKCR" Key="mozilla-vpn">
+          <RegistryValue Type="string" Name="URL Protocol" Value="mozilla-vpn"/>
+          <RegistryValue Type="string" Value="URL:mozilla-vpn Protocol"/>
+          <RegistryKey Key="DefaultIcon">
+            <RegistryValue Type="string" Value="MozillaVPN.exe" />
+          </RegistryKey>
+          <RegistryKey Key="shell\open\command">
+            <RegistryValue Type="string" Value="&quot;C:\Program Files\Mozilla\Mozilla VPN\MozillaVPN.exe&quot; &quot;%1&quot;" />
+          </RegistryKey>
+        </RegistryKey>
+
 				<!-- Broker Service -->
 				<ServiceInstall
 					Type="ownProcess"
 					Name="MozillaVPNBroker"
 					DisplayName="Mozilla VPN Broker"
@@ -110,11 +122,11 @@
 				<!-- Remove Tunnel Service, if running -->
 				<ServiceControl Id="MozillaVPNService" Remove="both" Name="WireGuardTunnel$MozillaVPN" Stop="both" Wait="yes" />
 			</Component>
 		</ComponentGroup>
 
-		<!--
+    <!--
 			Merge modules
 		-->
 		<DirectoryRef Id="MozillaVPNFolder">
 			<Merge Id="WintunMergeModule" Language="0" DiskId="1" SourceFile=".deps/wintun-$(var.WintunPlatform)-0.8.1.msm" />
 		</DirectoryRef>
diff --git a/ui/src/Firefox Private Network.csproj b/ui/src/Firefox Private Network.csproj
index f36a473..d380423 100644
--- a/ui/src/Firefox Private Network.csproj	
+++ b/ui/src/Firefox Private Network.csproj	
@@ -223,10 +223,16 @@
     <Reference Include="INIFileParser, Version=2.5.2.0, Culture=neutral, PublicKeyToken=79af7b307b65cf3c, processorArchitecture=MSIL">
       <HintPath>..\packages\ini-parser.2.5.2\lib\net20\INIFileParser.dll</HintPath>
       <EmbedInteropTypes>False</EmbedInteropTypes>
       <Private>True</Private>
     </Reference>
+    <Reference Include="Microsoft.IdentityModel.Logging, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Logging.6.7.1\lib\net461\Microsoft.IdentityModel.Logging.dll</HintPath>
+    </Reference>
+    <Reference Include="Microsoft.IdentityModel.Tokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
+      <HintPath>..\packages\Microsoft.IdentityModel.Tokens.6.7.1\lib\net461\Microsoft.IdentityModel.Tokens.dll</HintPath>
+    </Reference>
     <Reference Include="Microsoft.WindowsAPICodePack, Version=1.1.0.0, Culture=neutral, processorArchitecture=MSIL">
       <HintPath>..\packages\Microsoft.WindowsAPICodePack-Core.1.1.0.2\lib\Microsoft.WindowsAPICodePack.dll</HintPath>
     </Reference>
     <Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
       <HintPath>..\packages\Newtonsoft.Json.12.0.2\lib\net45\Newtonsoft.Json.dll</HintPath>
diff --git a/ui/src/FxA/Account.cs b/ui/src/FxA/Account.cs
index 0f66b2d..3336d74 100644
--- a/ui/src/FxA/Account.cs
+++ b/ui/src/FxA/Account.cs
@@ -48,10 +48,15 @@ namespace FirefoxPrivateNetwork.FxA
                 LoginState = LoginState.LoggedIn;
                 Avatar.InitializeCache(avatarUrl: Config.FxALogin.User.Avatar);
             }
         }
 
+        /// <summary>
+        /// Gets or sets the API version for making non-PKCE auth requests and handling responses.
+        /// </summary>
+        public static string OriginalApiVersion { get; set; } = "/api/v1";
+
         /// <summary>
         /// Gets or sets the config structure associated with this account.
         /// </summary>
         public Config Config { get; set; } = new Config();
 
@@ -71,11 +76,11 @@ namespace FirefoxPrivateNetwork.FxA
         /// <param name="fxaJson">JSON string received from FxA containing login data.</param>
         /// <returns>True on successful processing of the login response.</returns>
         public bool ProcessLogin(string fxaJson)
         {
             // Initialize a new login session configuration
-            Manager.Account.Config = new FxA.Config(fxaJson);
+            Manager.Account.Config = new Config(fxaJson);
 
             // Sets a value that a user has just logged in
             Manager.MainWindowViewModel.NewUserSignIn = true;
 
             // Generate a new WireGuard keypair in preparation for adding a new Account device
@@ -85,18 +90,18 @@ namespace FirefoxPrivateNetwork.FxA
             // Save login session to user's appdata folder
             Manager.Account.Config.SaveFxAToken();
             Manager.Account.Config.WriteFxAUserToFile(ProductConstants.FxAUserFile);
 
             // Set the account login state to logged in
-            Manager.Account.LoginState = FxA.LoginState.LoggedIn;
+            Manager.Account.LoginState = LoginState.LoggedIn;
 
             // Initialize cache for avatar image
             Manager.Account.Avatar.InitializeCache(avatarUrl: Config.FxALogin.User.Avatar);
 
             // Added a new account device through the FxA API, using the newly generated keypair
-            var devices = new FxA.Devices();
-            var deviceName = string.Format("{0} ({1} {2})", System.Environment.MachineName, System.Environment.OSVersion.Platform, System.Environment.OSVersion.Version);
+            var devices = new Devices();
+            var deviceName = string.Format("{0} ({1} {2})", Environment.MachineName, Environment.OSVersion.Platform, Environment.OSVersion.Version);
             var deviceAddResponse = devices.AddDevice(deviceName, keys.Public);
 
             // Upon successful addition of a new device, save the device interface to the WireGuard configuration file and IP addresses to settings file
             if (deviceAddResponse != null)
             {
@@ -157,11 +162,11 @@ namespace FirefoxPrivateNetwork.FxA
         /// Query the Fxa API for account details.
         /// </summary>
         /// <returns>FxA user JSON object.</returns>
         public JSONStructures.User GetAccountDetails()
         {
-            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/account", RestSharp.Method.GET);
+            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{OriginalApiVersion}/vpn/account", RestSharp.Method.GET);
 
             // Execute the request
             var response = api.SendRequest();
 
             if (response == null)
diff --git a/ui/src/FxA/Devices.cs b/ui/src/FxA/Devices.cs
index ef632b5..c97093e 100644
--- a/ui/src/FxA/Devices.cs
+++ b/ui/src/FxA/Devices.cs
@@ -2,10 +2,11 @@
 // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, you can obtain one at http://mozilla.org/MPL/2.0/.
 // </copyright>
 
 using System;
 using System.Collections.Generic;
+using System.IO;
 using System.Threading.Tasks;
 using Newtonsoft.Json;
 
 namespace FirefoxPrivateNetwork.FxA
 {
@@ -20,11 +21,11 @@ namespace FirefoxPrivateNetwork.FxA
         /// <param name="deviceName">Naming convention: machine name + platform + version.</param>
         /// <param name="publicKey">A unique key, part of a keypair generated using BCrypt algorithm.</param>
         /// <returns>Returns the user's device if successful, otherwise null.</returns>
         public JSONStructures.Device AddDevice(string deviceName, string publicKey)
         {
-            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device", RestSharp.Method.POST);
+            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device", RestSharp.Method.POST);
             api.AddPostBody(new Dictionary<string, string> { { "name", deviceName }, { "pubkey", publicKey } });
 
             // Execute the request
             var response = api.SendRequest();
 
@@ -69,11 +70,11 @@ namespace FirefoxPrivateNetwork.FxA
                 }
             }
 
             // Url encoding the device public key
             var sanitizedDevicePublicKey = System.Web.HttpUtility.UrlEncode(publicKey);
-            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
+            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
 
             // Execute the request`
             var response = api.SendRequest();
 
             if (response != null && response.StatusCode == System.Net.HttpStatusCode.NoContent)
diff --git a/ui/src/FxA/Login.cs b/ui/src/FxA/Login.cs
index 89954da..b938bc5 100644
--- a/ui/src/FxA/Login.cs
+++ b/ui/src/FxA/Login.cs
@@ -1,23 +1,34 @@
 ﻿// <copyright file="Login.cs" company="Mozilla">
 // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, you can obtain one at http://mozilla.org/MPL/2.0/.
 // </copyright>
 
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
+using System.IO;
 using System.Linq;
+using System.Security.Cryptography;
+using System.Security.Permissions;
+using System.Text;
 using System.Threading;
+using System.Web;
 using System.Windows;
+using Microsoft.IdentityModel.Tokens;
 using Newtonsoft.Json;
+using RestSharp.Extensions;
 
 namespace FirefoxPrivateNetwork.FxA
 {
     /// <summary>
     /// Handles login into FxA accounts.
     /// </summary>
     public class Login
     {
+        private static string codeVerifierPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
+        private static string codeVerifierFile = "moz_gen_cvf.txt";
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Login"/> class.
         /// </summary>
         /// <param name="handler">Handler for the login result event.</param>
         public Login(LoginResultHandler handler = null)
@@ -34,72 +45,39 @@ namespace FirefoxPrivateNetwork.FxA
         public delegate void LoginResultHandler(object sender, Login session, LoginState state);
 
         private event LoginResultHandler LoginResultEvent;
 
         /// <summary>
-        /// Gets the unique login urls for the user's signin attempt.
+        /// Gets or sets the API version for making PKCE auth requests and handling responses.
         /// </summary>
-        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
-        public JSONStructures.FxALoginURLs GetLoginURLs()
-        {
-            var api = new ApiRequest(string.Empty, "/vpn/login", RestSharp.Method.POST);
-            JSONStructures.FxALoginURLs loginURLs;
-
-            // Execute the request
-            var response = api.SendRequest();
-
-            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
-            {
-                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
-                return null;
-            }
-
-            try
-            {
-                loginURLs = JsonConvert.DeserializeObject<JSONStructures.FxALoginURLs>(response.Content);
-                return loginURLs;
-            }
-            catch (Exception e)
-            {
-                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
-            }
-
-            return null;
-        }
+        public static string ApiVersion { get; set; } = "/api/v2";
 
         /// <summary>
-        /// Queries the verification URL to check if the user has logged in.
+        /// Gets the unique login urls for the user's signin attempt.
         /// </summary>
-        /// <param name="tokenURL">Login verification URL.</param>
-        /// <returns>Returns the response content if successful, otherwise returns null.</returns>
-        public string QueryRawLoginState(string tokenURL)
+        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
+        public JSONStructures.FxALoginURLs GetLoginURL()
         {
-            var api = new ApiRequest(string.Empty, tokenURL, RestSharp.Method.GET);
+            // Generate code_verifier
+            string codeVerifier = GetUniqueToken(44);
 
-            // Execute the request
-            var response = api.SendRequest();
+            // Save the code_verifier for use in the verify request
+            File.WriteAllText(Path.Combine(codeVerifierPath, codeVerifierFile), codeVerifier);
 
-            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
-            {
-                ErrorHandling.ErrorHandler.Handle("User has not logged in yet", ErrorHandling.LogLevel.Debug);
-                return null;
-            }
+            // Generate the code_challenge by getting the SHA256 hash and encoding it to send in a request
+            // Switched the code_challenge_method to plain since there were intermittent issues with the SHA256 method returning the user data each time.
+            string codeChallenge = codeVerifier;
 
-            return response.Content;
-        }
+            // Make new instance of object that hold the URLs needed for a user to login
+            JSONStructures.FxALoginURLs loginURLs = new JSONStructures.FxALoginURLs();
 
-        /// <summary>
-        /// Deserializes the FxA login response from JSON.
-        /// </summary>
-        /// <param name="jsonContents">FxA login response.</param>
-        /// <returns>A <see cref="JSONStructures.FxALogin"/> object.</returns>
-        public JSONStructures.FxALogin ParseLoginState(string jsonContents)
-        {
             try
             {
-                var loginData = JsonConvert.DeserializeObject<JSONStructures.FxALogin>(jsonContents);
-                return loginData;
+                // The codeChallenge is there to handle the PKCE request to the server
+                loginURLs.LoginUrl = $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/windows?code_challenge={codeChallenge}&code_challenge_method=plain";
+
+                return loginURLs;
             }
             catch (Exception e)
             {
                 ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
             }
@@ -126,32 +104,39 @@ namespace FirefoxPrivateNetwork.FxA
         }
 
         /// <summary>
         /// Initiate the login attempt.
         /// </summary>
-        /// <param name="cancelToken">Token used to cancel the login process.</param>
         /// <returns>Whether the login process is started succefully.</returns>
-        public bool StartLogin(CancellationToken cancelToken)
+        public bool StartLogin()
         {
             try
             {
-                var loginURLs = GetLoginURLs();
-                if (loginURLs == null)
+                // Get the login and verify URLs
+                JSONStructures.FxALoginURLs loginURL = GetLoginURL();
+
+                if (loginURL == null)
                 {
                     return false;
                 }
 
-                var pollInterval = loginURLs.PollInterval % 31; // Max 30 seconds, no more
-                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
-                StartQueryLoginThread(loginURLs.VerificationUrl, loginURLs.PollInterval, loginURLs.ExpiresOn, cancelToken);
+                // Create the GET request for PKCE auth
+                ApiRequest api = new ApiRequest(string.Empty, loginURL.LoginUrl, RestSharp.Method.GET);
 
-                // Launch a browser
-                OpenBrowser(loginURLs.LoginUrl);
+                // Execute the request
+                var response = api.SendRequest();
 
-                // Navigate to verification page
-                UI.MainWindow mainWindow = (UI.MainWindow)Application.Current.MainWindow;
-                mainWindow.NavigateToView(new FirefoxPrivateNetwork.UI.VerifyAccountView(loginURLs.LoginUrl), UI.MainWindow.SlideDirection.Left);
+                if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
+                {
+                    ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
+                    return false;
+                }
+
+                Manager.Account.LoginState = LoginState.LoggingIn;
+
+                // Launch a browser
+                OpenBrowser(loginURL.LoginUrl);
             }
             catch (Exception e)
             {
                 ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-add-device-error"), ErrorHandling.UserFacingErrorType.Toast, ErrorHandling.UserFacingSeverity.ShowWarning, ErrorHandling.LogLevel.Error);
                 ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
@@ -160,102 +145,100 @@ namespace FirefoxPrivateNetwork.FxA
 
             return true;
         }
 
         /// <summary>
-        /// Intiates the login verification thread.
+        /// Gets verification code from mozilla-vpn:// redirect after the user logs in through the browser.
         /// </summary>
-        /// <param name="queryUri">Login verification URL.</param>
-        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
-        /// <param name="expiresAt">Expiration date of the verification URL.</param>
-        /// <param name="cancelToken">Token used to cancel the login thread.</param>
-        public void StartQueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
+        /// <param name="code">Verification code from the custom URL protocol redirect after logging in.</param>
+        public void VerifyUserLogin(string code)
         {
-            var loginThread = new Thread(() => QueryLoginThread(queryUri, timeoutSeconds, expiresAt, cancelToken))
+            // Make the POST request to the verify endpoint
+            ApiRequest api = new ApiRequest(string.Empty, $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/verify", RestSharp.Method.POST);
+
+            // Get code_verifier that was generated in the login request
+            string code_verifier = File.ReadAllText(Path.Combine(codeVerifierPath, codeVerifierFile));
+
+            // Create a new dictionary to hold the code and code_verifier.
+            Dictionary<string, string> postBody = new Dictionary<string, string>();
+            postBody.Add("code", code);
+            postBody.Add("code_verifier", code_verifier);
+            api.AddPostBody(postBody);
+
+            // Execute the request
+            var response = api.SendRequest();
+
+            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
             {
-                IsBackground = true,
-            };
-            loginThread.Start();
+                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
+                return;
+            }
+
+            // Check login credentials, add the session info to the settings, and add a new device
+            Manager.Account.ProcessLogin(response.Content);
         }
 
         /// <summary>
-        /// Polls a verification URL periodically to confirm if the user has logged in.
+        /// Generates a Base64 encoded string based on a SHA256 hash with the string it's given.
         /// </summary>
-        /// <param name="queryUri">Login verification URL.</param>
-        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
-        /// <param name="expiresAt">Expiration date of the verification URL.</param>
-        /// <param name="cancelToken">Token used to cancel the login thread.</param>
-        private void QueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
+        /// <param name="text">The text that needs to be converted to a SHA256 hash.</param>
+        /// <returns>A Base64 encoded SHA256 hash.</returns>
+        private static string GetHashSha256(string text)
         {
-            while (Manager.Account.LoginState == FxA.LoginState.LoggingIn && DateTime.Compare(DateTime.UtcNow, expiresAt) < 0)
-            {
-                try
-                {
-                    var queryRawData = QueryRawLoginState(queryUri);
+            SHA256Managed hashTool = new SHA256Managed();
 
-                    if (cancelToken.IsCancellationRequested)
-                    {
-                        break;
-                    }
+            byte[] phraseAsByte = Encoding.UTF8.GetBytes(string.Concat(text));
+            byte[] encryptedBytes = hashTool.ComputeHash(phraseAsByte);
 
-                    if (queryRawData == null)
-                    {
-                        Thread.Sleep(TimeSpan.FromSeconds(timeoutSeconds));
-                        continue;
-                    }
+            hashTool.Clear();
 
-                    var queryData = ParseLoginState(queryRawData);
+            string value = Convert.ToBase64String(encryptedBytes);
 
-                    if (queryData.User == null || queryData.User.Subscriptions == null || queryData.User.Subscriptions.Vpn == null)
-                    {
-                        Manager.Account.LoginState = LoginState.LoggedOut;
-                        break;
-                    }
+            return value;
+        }
 
-                    if (!queryData.User.Subscriptions.Vpn.Active)
-                    {
-                        Manager.Account.LoginState = LoginState.LoggedOut;
-                        break;
-                    }
+        /// <summary>
+        /// Generates a random cryptographic string to use in a PKCE auth request and response.
+        /// </summary>
+        /// <param name="length">The number of characters that should be in the string.</param>
+        /// <param name="chars">The acceptable characters that can be in the string.</param>
+        /// <returns>A cryptographic string.</returns>
+        private static string GetUniqueToken(int length, string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_")
+        {
+            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
+            {
+                byte[] data = new byte[length];
 
-                    var processLoginResult = Manager.Account.ProcessLogin(queryRawData);
-                    Manager.Account.LoginState = LoginState.LoggedIn;
-                    Manager.StartUIUpdaters();
+                // If chars.Length isn't a power of 2 then there is a bias if we simply use the modulus operator. The first characters of chars will be more probable than the last ones.
+                // buffer used if we encounter an unusable random byte. We will regenerate it in this buffer.
+                byte[] buffer = null;
 
-                    var maxDevicesReached = !processLoginResult && Manager.Account.Config.FxALogin.User.Devices.Count() >= Manager.Account.Config.FxALogin.User.MaxDevices;
+                // Maximum random number that can be used without introducing a bias
+                int maxRandom = byte.MaxValue - ((byte.MaxValue + 1) % chars.Length);
 
-                    Cache.FxAServerList.RetrieveRemoteServerList();
-                    Application.Current.Dispatcher.Invoke(() =>
+                crypto.GetBytes(data);
+
+                char[] result = new char[length];
+
+                for (int i = 0; i < length; i++)
+                {
+                    byte value = data[i];
+
+                    while (value > maxRandom)
                     {
-                        var owner = Application.Current.MainWindow;
-                        if (owner != null)
+                        if (buffer == null)
                         {
-                            if (!Manager.MustUpdate)
-                            {
-                                if (maxDevicesReached)
-                                {
-                                    ((UI.MainWindow)owner).NavigateToView(new UI.DevicesView(deviceLimitReached: true, fxaJson: queryRawData), UI.MainWindow.SlideDirection.Left);
-                                }
-                                else
-                                {
-                                    ((UI.MainWindow)owner).NavigateToView(new UI.OnboardingView5(), UI.MainWindow.SlideDirection.Left);
-                                }
-                            }
-
-                            ((UI.MainWindow)owner).Show();
-                            ((UI.MainWindow)owner).WindowState = WindowState.Normal;
-                            ((UI.MainWindow)owner).Activate();
+                            buffer = new byte[1];
                         }
-                    });
-                }
-                catch (Exception e)
-                {
-                    ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Debug);
+
+                        crypto.GetBytes(buffer);
+                        value = buffer[0];
+                    }
+
+                    result[i] = chars[value % chars.Length];
                 }
 
-                Thread.Sleep(TimeSpan.FromSeconds(5));
+                return new string(result);
             }
-
-            LoginResultEvent?.Invoke(this, this, Manager.Account.LoginState);
         }
     }
 }
diff --git a/ui/src/FxA/LoginSessionManager.cs b/ui/src/FxA/LoginSessionManager.cs
index f0cd66d..f3befe6 100644
--- a/ui/src/FxA/LoginSessionManager.cs
+++ b/ui/src/FxA/LoginSessionManager.cs
@@ -30,11 +30,11 @@ namespace FirefoxPrivateNetwork.FxA
                 Login login = new Login(LoginResultCallback);
                 CancellationTokenSource tokenSource = new CancellationTokenSource();
 
                 sessions.Add(login, tokenSource);
 
-                if (!login.StartLogin(tokenSource.Token))
+                if (!login.StartLogin())
                 {
                     CancelCurrentSession();
                 }
             });
         }
diff --git a/ui/src/FxA/ServerList/ServerList.cs b/ui/src/FxA/ServerList/ServerList.cs
index f81dd9e..22167e6 100644
--- a/ui/src/FxA/ServerList/ServerList.cs
+++ b/ui/src/FxA/ServerList/ServerList.cs
@@ -171,11 +171,11 @@ namespace FirefoxPrivateNetwork.FxA
         /// Retrieves the VPN server list from the FxA API.
         /// </summary>
         /// <returns>Success status of the remote server list retrieval.</returns>
         public bool RetrieveRemoteServerList()
         {
-            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/servers", Method.GET);
+            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/servers", Method.GET);
             var response = api.SendRequest();
             if (response == null)
             {
                 return false;
             }
diff --git a/ui/src/JSONStructures/Login/FxALoginURLs.cs b/ui/src/JSONStructures/Login/FxALoginURLs.cs
index e3517a4..aa3b040 100644
--- a/ui/src/JSONStructures/Login/FxALoginURLs.cs
+++ b/ui/src/JSONStructures/Login/FxALoginURLs.cs
@@ -21,19 +21,7 @@ namespace FirefoxPrivateNetwork.JSONStructures
         /// <summary>
         /// Gets or sets the verification URL to poll.
         /// </summary>
         [JsonProperty("verification_url")]
         public string VerificationUrl { get; set; }
-
-        /// <summary>
-        /// Gets or sets the expiration date/time for the URLs.
-        /// </summary>
-        [JsonProperty("expires_on")]
-        public DateTime ExpiresOn { get; set; }
-
-        /// <summary>
-        /// Gets or sets the recommended poll interval for the verification URL.
-        /// </summary>
-        [JsonProperty("poll_interval")]
-        public int PollInterval { get; set; }
     }
 }
diff --git a/ui/src/Main.cs b/ui/src/Main.cs
index 0eff0e5..b842339 100644
--- a/ui/src/Main.cs
+++ b/ui/src/Main.cs
@@ -1,10 +1,12 @@
 ﻿// <copyright file="Main.cs" company="Mozilla">
 // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, you can obtain one at http://mozilla.org/MPL/2.0/.
 // </copyright>
 
 using System;
+using System.Collections.Generic;
+using System.Diagnostics;
 using System.Linq;
 using System.ServiceProcess;
 using System.Threading;
 using System.Windows;
 using FirefoxPrivateNetwork.WCF;
@@ -15,26 +17,23 @@ namespace FirefoxPrivateNetwork
 {
     /// <summary>
     /// Entry point of the Mozilla VPN application.
     /// </summary>
     [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.StyleCop.CSharp.DocumentationRules", "SA1649:FileNameMustMatchTypeName", Justification = "Default C# behavior.")]
-    internal class Entry : System.Windows.Application
+    internal class Entry : Application
     {
-        /// <summary>
-        /// Global value that is used to indicate if there is already an instance of the application running.
-        /// </summary>
-        private static readonly Mutex RunOnceMutex = new Mutex(false, string.Concat(@"Local\", ProductConstants.GUID));
-
         /// <summary>
         /// Main entry point of the application.
         /// </summary>
         /// <param name="args">System provided command line parameters.</param>
         public static void Main(string[] args)
         {
             bool ranOnStartup = false;
 
-            if (args.Count() == 1)
+            List<Process> processes = Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).ToList();
+
+            if (args.Count() == 1 && !args[0].Contains("mozilla-vpn:"))
             {
                 // Run the broker child process, skip the UI
                 if (args.First().ToLower() == "broker")
                 {
                     RunBroker();
@@ -57,20 +56,21 @@ namespace FirefoxPrivateNetwork
                     return;
                 }
             }
 
             // Prevent multiple instances of the application
-            if (!RunOnceMutex.WaitOne(TimeSpan.Zero, true))
+            if (processes.Count > 1)
             {
-                // Already running, attempt to send a "show" command to the already running process before exiting
-                var runningWindow = User32.FindWindow(ProductConstants.TrayWindowClassName, string.Empty);
-                if (runningWindow != IntPtr.Zero)
+                processes.Sort((x, y) => DateTime.Compare(y.StartTime, x.StartTime));
+
+                if (processes.Count > 2)
                 {
-                    User32.SendMessage(runningWindow, User32.WmShow, IntPtr.Zero, string.Empty);
+                    for (int i = 1; i <= processes.Count - 2; i++)
+                    {
+                        processes[i].Kill();
+                    }
                 }
-
-                Environment.Exit(1);
             }
 
             // We dont need `If Debug_QA` here, because we already had an attribute `[Conditional("DEBUG_QA")]` for this function
             Tester.OpenConnection();
 
@@ -79,11 +79,21 @@ namespace FirefoxPrivateNetwork
                 // Main Application
                 var app = new App();
                 app.InitializeComponent();
 
                 // Initialize interfaces
-                Manager.Initialize();
+                // Run this if the app is opened via the custom URL protocol with a code
+                if (args.Count() == 1 && args[0].Contains("mozilla-vpn:"))
+                {
+                    // Get the code string that's returned from the server to be used in the verification request
+                    Manager.Initialize(args.First());
+                    VerifyUser(args.First());
+                }
+                else
+                {
+                    Manager.Initialize();
+                }
 
                 // Has the app just been launched at Windows startup?
                 Manager.MainWindowViewModel.RanOnStartup = ranOnStartup;
 
                 // Run the application
@@ -134,7 +144,34 @@ namespace FirefoxPrivateNetwork
             {
                 ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
                 Environment.Exit(1);
             }
         }
+
+        /// <summary>
+        /// Starts the WireGuard tunnel service.
+        /// </summary>
+        /// <param name="args">
+        /// Argument 0: "tunnel" keyword
+        /// Argument 1: Path to the configuration file.
+        /// </param>
+        private static void VerifyUser(string args)
+        {
+            try
+            {
+                string code = args.Substring(args.IndexOf("code=") + 5);
+
+                if (code != null && code.Length >= 44)
+                {
+                    FxA.Login verifyUser = new FxA.Login();
+
+                    verifyUser.VerifyUserLogin(code);
+                }
+            }
+            catch (Exception e)
+            {
+                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
+                Environment.Exit(1);
+            }
+        }
     }
 }
diff --git a/ui/src/Manager.cs b/ui/src/Manager.cs
index 93de63f..42bf7b8 100644
--- a/ui/src/Manager.cs
+++ b/ui/src/Manager.cs
@@ -109,19 +109,20 @@ namespace FirefoxPrivateNetwork
         public static Settings Settings { get; set; }
 
         /// <summary>
         /// Initialize all elements of the Manager class.
         /// </summary>
-        public static void Initialize()
+        /// <param name="code">Optional verification code.</param>
+        public static void Initialize(string code = "")
         {
             InitializeSettings();
             InitializeTranslationService();
             InitializeTray();
             InitializeServerListCache();
             InitializeTunnel();
             InitializeAccount();
-            InitializeViewModels();
+            InitializeViewModels(code);
             InitializeWlanWatcher();
             InitializeCaptivePortalDetector();
             InitializeIpInfo();
             InitializeUIUpdaters();
         }
@@ -165,13 +166,14 @@ namespace FirefoxPrivateNetwork
         }
 
         /// <summary>
         /// Initialize the main window view model.
         /// </summary>
-        public static void InitializeViewModels()
+        /// <param name="code">Optional verification code.</param>
+        public static void InitializeViewModels(string code = "")
         {
-            MainWindowViewModel = new ViewModels.MainWindowViewModel();
+            MainWindowViewModel = new ViewModels.MainWindowViewModel(code);
         }
 
         /// <summary>
         /// Initialize the wireless local area network watcher.
         /// </summary>
@@ -231,10 +233,12 @@ namespace FirefoxPrivateNetwork
         public static void TerminateUIUpdaters()
         {
             ConnectionStatusUpdater.StopThread();
             ServerListUpdater.StopThread();
             AccountInfoUpdater.StopTask();
+            VersionUpdater.StopTask();
+            ToastManager.StopThread();
         }
 
         /// <summary>
         /// Initializes the user's FxA account.
         /// </summary>
diff --git a/ui/src/ProductConstants.cs b/ui/src/ProductConstants.cs
index 7eb2b8b..b7096c7 100644
--- a/ui/src/ProductConstants.cs
+++ b/ui/src/ProductConstants.cs
@@ -311,14 +311,14 @@ namespace FirefoxPrivateNetwork
                 IsDevMode = true;
                 BaseUrl = customBaseURL;
             }
 
             // FxA API link
-            FxAUrl = string.Concat(BaseUrl, "/api/v1");
+            FxAUrl = string.Concat(BaseUrl, "/api/v2");
 
             // Login link
-            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login");
+            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login/windows");
 
             // Feedback form link
             FeedbackFormUrl = string.Concat(BaseUrl, "/r/vpn/client/feedback", FxAUtmTags);
 
             // Subscription Link
diff --git a/ui/src/UI/Components/Card/Hero/HeroSubtext.xaml b/ui/src/UI/Components/Card/Hero/HeroSubtext.xaml
index 190c02b..c86bb22 100644
--- a/ui/src/UI/Components/Card/Hero/HeroSubtext.xaml
+++ b/ui/src/UI/Components/Card/Hero/HeroSubtext.xaml
@@ -120,11 +120,11 @@
                                     </MultiBinding>
                                 </Setter.Value>
                             </Setter>
                         </DataTrigger>
 
-                        <!-- Connection unsable -->
+                        <!-- Connection unstable -->
                         <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:HeroSubText}}, Path=(extension:CardExtensions.VpnStability)}" Value="1">
                             <Setter Property="Text">
                                 <Setter.Value>
                                     <MultiBinding StringFormat="{}  •  {0}">
                                         <Binding Path="[hero-subtext-check-connection]" />
diff --git a/ui/src/UI/Resources/Localization/Translations/en-us.ftl b/ui/src/UI/Resources/Localization/Translations/en-us.ftl
index 10bb8ca..8ca1224 100644
--- a/ui/src/UI/Resources/Localization/Translations/en-us.ftl
+++ b/ui/src/UI/Resources/Localization/Translations/en-us.ftl
@@ -126,11 +126,11 @@ devices-add-date-years = {$numYears ->
 devices-of = of
 devices-page-description = Devices with {application-name} installed using your account. Connect up to {$numMaxDevices} devices.
 devices-limit-reached-title = Remove a device
 devices-limit-reached-content = You've reached your limit. To install the VPN on this device, you'll need to remove one.
 devices-remove-popup-title = Remove device?
-devices-remove-popup-content = Please confirm you would like to remove {$deviceName}.
+devices-remove-popup-content = Please confirm you would like to remove {$deviceName}. You will need to open the VPN client on the removed device to clear your network settings and access the internet again.
 devices-remove = Remove device
 
 ## Popup
 popup-cancel-button-text = Cancel
 popup-remove-button-text = Remove
diff --git a/ui/src/UI/Views/Settings/SettingsView.xaml b/ui/src/UI/Views/Settings/SettingsView.xaml
index 27f774b..6b78283 100644
--- a/ui/src/UI/Views/Settings/SettingsView.xaml
+++ b/ui/src/UI/Views/Settings/SettingsView.xaml
@@ -80,11 +80,11 @@
                 </Grid>
 
                 <!-- Launch VPN on startup setting -->
                 <Grid HorizontalAlignment="Left" VerticalAlignment="Top" Grid.Row="1" Grid.Column="0">
                     <StackPanel Margin="0,0,0,8">
-                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click">
+                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click" Visibility="Hidden">
                             <CheckBox.Style>
                                 <Style BasedOn="{StaticResource Checkbox}" TargetType="{x:Type CheckBox}">
                                     <Setter Property="IsChecked" Value="False" />
                                     <Style.Triggers>
                                         <DataTrigger Binding="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type local:SettingsView}}, Path=RunOnStartup}" Value="True">
diff --git a/ui/src/ViewModels/MainWindowViewModel.cs b/ui/src/ViewModels/MainWindowViewModel.cs
index f3ae6c5..f9a14ae 100644
--- a/ui/src/ViewModels/MainWindowViewModel.cs
+++ b/ui/src/ViewModels/MainWindowViewModel.cs
@@ -73,11 +73,12 @@ namespace FirefoxPrivateNetwork.ViewModels
         private List<CultureInfo> additionalLanguagesList;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="MainWindowViewModel"/> class.
         /// </summary>
-        public MainWindowViewModel()
+        /// <param name="code">Optional verification code.</param>
+        public MainWindowViewModel(string code)
         {
             // Initialize connectionStatus in the view model
             connectionStatus = new Models.ConnectionStatus
             {
                 Status = Models.ConnectionState.Unprotected,
@@ -97,11 +98,15 @@ namespace FirefoxPrivateNetwork.ViewModels
                 currentDevice = Manager.Account.Config.FxALogin.User.Devices.Find(d => d.PublicKey == Manager.Account.Config.FxALogin.PublicKey);
                 UserNumDevices = Manager.Account.Config.FxALogin.User.Devices.Count();
             }
 
             // Determine initial page to display in the ViewFrame
-            if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
+            if (code.Length >= 44)
+            {
+                InitialViewFrameSourceType = typeof(UI.OnboardingView5);
+            }
+            else if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
             {
                 InitialViewFrameSourceType = typeof(UI.MainView);
             }
             else
             {
diff --git a/ui/src/WCF/IService.cs b/ui/src/WCF/IService.cs
index 1fb17ca..5d5c28a 100644
--- a/ui/src/WCF/IService.cs
+++ b/ui/src/WCF/IService.cs
@@ -39,19 +39,10 @@ namespace FirefoxPrivateNetwork.WCF
         /// <returns>WCF response.</returns>
         [OperationContract]
         [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
         Response Disconnect();
 
-        /// <summary>
-        /// Login stub.
-        /// </summary>
-        /// <param name="req">Login request object.</param>
-        /// <returns>WCF response.</returns>
-        [OperationContract]
-        [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
-        Response Login(LoginRequest req);
-
         /// <summary>
         /// Logout stub.
         /// </summary>
         /// <returns>WCF response.</returns>
         [OperationContract]
diff --git a/ui/src/WCF/Service.cs b/ui/src/WCF/Service.cs
index 8d73b0c..6e97075 100644
--- a/ui/src/WCF/Service.cs
+++ b/ui/src/WCF/Service.cs
@@ -116,31 +116,10 @@ namespace FirefoxPrivateNetwork.WCF
             {
                 return new Response(500, ex.Message);
             }
         }
 
-        /// <summary>
-        /// Initiate login.
-        /// </summary>
-        /// <param name="req">LoginRequest WCF command object.</param>
-        /// <returns>WCF response.</returns>
-        public Response Login(LoginRequest req)
-        {
-            try
-            {
-                var loginInstance = new FxA.Login();
-                var pollInterval = req.PollInterval % 31; // Max 30 seconds, no more
-                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
-                loginInstance.StartQueryLoginThread(req.VerificationUrl, req.PollInterval, req.ExpiresOn, CancellationToken.None);
-                return new Response(200, "Success");
-            }
-            catch (Exception ex)
-            {
-                return new Response(500, ex.Message);
-            }
-        }
-
         /// <summary>
         /// Initiate logout.
         /// </summary>
         /// <returns>WCF response.</returns>
         public Response Logout()
diff --git a/ui/src/app.manifest b/ui/src/app.manifest
index 0d11c3a..0b10643 100644
--- a/ui/src/app.manifest
+++ b/ui/src/app.manifest
@@ -2,11 +2,12 @@
 <assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
   <assemblyIdentity version="0.1.2.2" name="MozillaVPN.app"/>
   <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
     <security>
       <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
-        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
+        <requestedExecutionLevel level="highestAvailable" uiAccess="false" />
+        <!--<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />-->
       </requestedPrivileges>
     </security>
   </trustInfo>
 
   <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
diff --git a/ui/src/packages.config b/ui/src/packages.config
index 15b03f3..f258bbb 100644
--- a/ui/src/packages.config
+++ b/ui/src/packages.config
@@ -3,10 +3,12 @@
   <package id="Costura.Fody" version="4.1.0" targetFramework="net46" />
   <package id="DotNetZip" version="1.11.0" targetFramework="net472" />
   <package id="Fluent.Net" version="1.0.31" targetFramework="net472" />
   <package id="Fody" version="6.0.0" targetFramework="net46" developmentDependency="true" />
   <package id="ini-parser" version="2.5.2" targetFramework="net472" />
+  <package id="Microsoft.IdentityModel.Logging" version="6.7.1" targetFramework="net472" />
+  <package id="Microsoft.IdentityModel.Tokens" version="6.7.1" targetFramework="net472" />
   <package id="Microsoft.NETCore.Platforms" version="2.2.2" targetFramework="net472" />
   <package id="Microsoft.WindowsAPICodePack-Core" version="1.1.0.2" targetFramework="net472" />
   <package id="Newtonsoft.Json" version="12.0.2" targetFramework="net472" />
   <package id="NodaTime" version="2.4.7" targetFramework="net472" />
   <package id="RestSharp" version="106.10.1" targetFramework="net472" />

Vulnerable code: 		<!--
            Manager.Account.Config = new FxA.Config(fxaJson);
            Manager.Account.LoginState = FxA.LoginState.LoggedIn;
            var devices = new FxA.Devices();
            var deviceName = string.Format("{0} ({1} {2})", System.Environment.MachineName, System.Environment.OSVersion.Platform, System.Environment.OSVersion.Version);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/account", RestSharp.Method.GET);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device", RestSharp.Method.POST);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
        /// Gets the unique login urls for the user's signin attempt.
        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
        public JSONStructures.FxALoginURLs GetLoginURLs()
        {
            var api = new ApiRequest(string.Empty, "/vpn/login", RestSharp.Method.POST);
            JSONStructures.FxALoginURLs loginURLs;

            // Execute the request
            var response = api.SendRequest();

            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
            {
                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                return null;
            }

            try
            {
                loginURLs = JsonConvert.DeserializeObject<JSONStructures.FxALoginURLs>(response.Content);
                return loginURLs;
            }
            catch (Exception e)
            {
                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
            }

            return null;
        }
        /// Queries the verification URL to check if the user has logged in.
        /// <param name="tokenURL">Login verification URL.</param>
        /// <returns>Returns the response content if successful, otherwise returns null.</returns>
        public string QueryRawLoginState(string tokenURL)
            var api = new ApiRequest(string.Empty, tokenURL, RestSharp.Method.GET);
            // Execute the request
            var response = api.SendRequest();
            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
            {
                ErrorHandling.ErrorHandler.Handle("User has not logged in yet", ErrorHandling.LogLevel.Debug);
                return null;
            }
            return response.Content;
        }
        /// <summary>
        /// Deserializes the FxA login response from JSON.
        /// </summary>
        /// <param name="jsonContents">FxA login response.</param>
        /// <returns>A <see cref="JSONStructures.FxALogin"/> object.</returns>
        public JSONStructures.FxALogin ParseLoginState(string jsonContents)
        {
                var loginData = JsonConvert.DeserializeObject<JSONStructures.FxALogin>(jsonContents);
                return loginData;
        /// <param name="cancelToken">Token used to cancel the login process.</param>
        public bool StartLogin(CancellationToken cancelToken)
                var loginURLs = GetLoginURLs();
                if (loginURLs == null)
                var pollInterval = loginURLs.PollInterval % 31; // Max 30 seconds, no more
                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
                StartQueryLoginThread(loginURLs.VerificationUrl, loginURLs.PollInterval, loginURLs.ExpiresOn, cancelToken);
                // Launch a browser
                OpenBrowser(loginURLs.LoginUrl);
                // Navigate to verification page
                UI.MainWindow mainWindow = (UI.MainWindow)Application.Current.MainWindow;
                mainWindow.NavigateToView(new FirefoxPrivateNetwork.UI.VerifyAccountView(loginURLs.LoginUrl), UI.MainWindow.SlideDirection.Left);
        /// Intiates the login verification thread.
        /// <param name="queryUri">Login verification URL.</param>
        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
        /// <param name="expiresAt">Expiration date of the verification URL.</param>
        /// <param name="cancelToken">Token used to cancel the login thread.</param>
        public void StartQueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
            var loginThread = new Thread(() => QueryLoginThread(queryUri, timeoutSeconds, expiresAt, cancelToken))
                IsBackground = true,
            };
            loginThread.Start();
        /// Polls a verification URL periodically to confirm if the user has logged in.
        /// <param name="queryUri">Login verification URL.</param>
        /// <param name="timeoutSeconds">Timeout (secs) between verification query attempts.</param>
        /// <param name="expiresAt">Expiration date of the verification URL.</param>
        /// <param name="cancelToken">Token used to cancel the login thread.</param>
        private void QueryLoginThread(string queryUri, int timeoutSeconds, DateTime expiresAt, CancellationToken cancelToken)
            while (Manager.Account.LoginState == FxA.LoginState.LoggingIn && DateTime.Compare(DateTime.UtcNow, expiresAt) < 0)
            {
                try
                {
                    var queryRawData = QueryRawLoginState(queryUri);
                    if (cancelToken.IsCancellationRequested)
                    {
                        break;
                    }
                    if (queryRawData == null)
                    {
                        Thread.Sleep(TimeSpan.FromSeconds(timeoutSeconds));
                        continue;
                    }
                    var queryData = ParseLoginState(queryRawData);
                    if (queryData.User == null || queryData.User.Subscriptions == null || queryData.User.Subscriptions.Vpn == null)
                    {
                        Manager.Account.LoginState = LoginState.LoggedOut;
                        break;
                    }
                    if (!queryData.User.Subscriptions.Vpn.Active)
                    {
                        Manager.Account.LoginState = LoginState.LoggedOut;
                        break;
                    }
                    var processLoginResult = Manager.Account.ProcessLogin(queryRawData);
                    Manager.Account.LoginState = LoginState.LoggedIn;
                    Manager.StartUIUpdaters();
                    var maxDevicesReached = !processLoginResult && Manager.Account.Config.FxALogin.User.Devices.Count() >= Manager.Account.Config.FxALogin.User.MaxDevices;
                    Cache.FxAServerList.RetrieveRemoteServerList();
                    Application.Current.Dispatcher.Invoke(() =>
                        var owner = Application.Current.MainWindow;
                        if (owner != null)
                            if (!Manager.MustUpdate)
                            {
                                if (maxDevicesReached)
                                {
                                    ((UI.MainWindow)owner).NavigateToView(new UI.DevicesView(deviceLimitReached: true, fxaJson: queryRawData), UI.MainWindow.SlideDirection.Left);
                                }
                                else
                                {
                                    ((UI.MainWindow)owner).NavigateToView(new UI.OnboardingView5(), UI.MainWindow.SlideDirection.Left);
                                }
                            }

                            ((UI.MainWindow)owner).Show();
                            ((UI.MainWindow)owner).WindowState = WindowState.Normal;
                            ((UI.MainWindow)owner).Activate();
                    });
                }
                catch (Exception e)
                {
                    ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Debug);
                Thread.Sleep(TimeSpan.FromSeconds(5));

            LoginResultEvent?.Invoke(this, this, Manager.Account.LoginState);
                if (!login.StartLogin(tokenSource.Token))
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, "/vpn/servers", Method.GET);

        /// <summary>
        /// Gets or sets the expiration date/time for the URLs.
        /// </summary>
        [JsonProperty("expires_on")]
        public DateTime ExpiresOn { get; set; }

        /// <summary>
        /// Gets or sets the recommended poll interval for the verification URL.
        /// </summary>
        [JsonProperty("poll_interval")]
        public int PollInterval { get; set; }
    internal class Entry : System.Windows.Application
        /// <summary>
        /// Global value that is used to indicate if there is already an instance of the application running.
        /// </summary>
        private static readonly Mutex RunOnceMutex = new Mutex(false, string.Concat(@"Local\", ProductConstants.GUID));

            if (args.Count() == 1)
            if (!RunOnceMutex.WaitOne(TimeSpan.Zero, true))
                // Already running, attempt to send a "show" command to the already running process before exiting
                var runningWindow = User32.FindWindow(ProductConstants.TrayWindowClassName, string.Empty);
                if (runningWindow != IntPtr.Zero)
                    User32.SendMessage(runningWindow, User32.WmShow, IntPtr.Zero, string.Empty);

                Environment.Exit(1);
                Manager.Initialize();
        public static void Initialize()
            InitializeViewModels();
        public static void InitializeViewModels()
            MainWindowViewModel = new ViewModels.MainWindowViewModel();
            FxAUrl = string.Concat(BaseUrl, "/api/v1");
            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login");
                        <!-- Connection unsable -->
devices-remove-popup-content = Please confirm you would like to remove {$deviceName}.
                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click">
        public MainWindowViewModel()
            if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
        /// <summary>
        /// Login stub.
        /// </summary>
        /// <param name="req">Login request object.</param>
        /// <returns>WCF response.</returns>
        [OperationContract]
        [WebInvoke(Method = "POST", ResponseFormat = WebMessageFormat.Json, RequestFormat = WebMessageFormat.Json)]
        Response Login(LoginRequest req);

        /// <summary>
        /// Initiate login.
        /// </summary>
        /// <param name="req">LoginRequest WCF command object.</param>
        /// <returns>WCF response.</returns>
        public Response Login(LoginRequest req)
        {
            try
            {
                var loginInstance = new FxA.Login();
                var pollInterval = req.PollInterval % 31; // Max 30 seconds, no more
                Manager.Account.LoginState = FxA.LoginState.LoggingIn;
                loginInstance.StartQueryLoginThread(req.VerificationUrl, req.PollInterval, req.ExpiresOn, CancellationToken.None);
                return new Response(200, "Success");
            }
            catch (Exception ex)
            {
                return new Response(500, ex.Message);
            }
        }

        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
Fixed code:         <!-- Custom URL protocol for handling the PKCE auth flow -->
        <RegistryKey Root="HKCR" Key="mozilla-vpn">
          <RegistryValue Type="string" Name="URL Protocol" Value="mozilla-vpn"/>
          <RegistryValue Type="string" Value="URL:mozilla-vpn Protocol"/>
          <RegistryKey Key="DefaultIcon">
            <RegistryValue Type="string" Value="MozillaVPN.exe" />
          </RegistryKey>
          <RegistryKey Key="shell\open\command">
            <RegistryValue Type="string" Value="&quot;C:\Program Files\Mozilla\Mozilla VPN\MozillaVPN.exe&quot; &quot;%1&quot;" />
          </RegistryKey>
        </RegistryKey>

    <!--
    <Reference Include="Microsoft.IdentityModel.Logging, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.IdentityModel.Logging.6.7.1\lib\net461\Microsoft.IdentityModel.Logging.dll</HintPath>
    </Reference>
    <Reference Include="Microsoft.IdentityModel.Tokens, Version=6.7.1.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
      <HintPath>..\packages\Microsoft.IdentityModel.Tokens.6.7.1\lib\net461\Microsoft.IdentityModel.Tokens.dll</HintPath>
    </Reference>
        /// <summary>
        /// Gets or sets the API version for making non-PKCE auth requests and handling responses.
        /// </summary>
        public static string OriginalApiVersion { get; set; } = "/api/v1";

            Manager.Account.Config = new Config(fxaJson);
            Manager.Account.LoginState = LoginState.LoggedIn;
            var devices = new Devices();
            var deviceName = string.Format("{0} ({1} {2})", Environment.MachineName, Environment.OSVersion.Platform, Environment.OSVersion.Version);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{OriginalApiVersion}/vpn/account", RestSharp.Method.GET);
using System.IO;
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device", RestSharp.Method.POST);
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/device/" + sanitizedDevicePublicKey, RestSharp.Method.DELETE);
using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography;
using System.Security.Permissions;
using System.Text;
using System.Web;
using Microsoft.IdentityModel.Tokens;
using RestSharp.Extensions;
        private static string codeVerifierPath = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData);
        private static string codeVerifierFile = "moz_gen_cvf.txt";

        /// Gets or sets the API version for making PKCE auth requests and handling responses.
        public static string ApiVersion { get; set; } = "/api/v2";
        /// Gets the unique login urls for the user's signin attempt.
        /// <returns>A <see cref="JSONStructures.FxALoginURLs"/> object.</returns>
        public JSONStructures.FxALoginURLs GetLoginURL()
            // Generate code_verifier
            string codeVerifier = GetUniqueToken(44);
            // Save the code_verifier for use in the verify request
            File.WriteAllText(Path.Combine(codeVerifierPath, codeVerifierFile), codeVerifier);
            // Generate the code_challenge by getting the SHA256 hash and encoding it to send in a request
            // Switched the code_challenge_method to plain since there were intermittent issues with the SHA256 method returning the user data each time.
            string codeChallenge = codeVerifier;
            // Make new instance of object that hold the URLs needed for a user to login
            JSONStructures.FxALoginURLs loginURLs = new JSONStructures.FxALoginURLs();
                // The codeChallenge is there to handle the PKCE request to the server
                loginURLs.LoginUrl = $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/windows?code_challenge={codeChallenge}&code_challenge_method=plain";

                return loginURLs;
        public bool StartLogin()
                // Get the login and verify URLs
                JSONStructures.FxALoginURLs loginURL = GetLoginURL();

                if (loginURL == null)
                // Create the GET request for PKCE auth
                ApiRequest api = new ApiRequest(string.Empty, loginURL.LoginUrl, RestSharp.Method.GET);
                // Execute the request
                var response = api.SendRequest();
                if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
                {
                    ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                    return false;
                }

                Manager.Account.LoginState = LoginState.LoggingIn;

                // Launch a browser
                OpenBrowser(loginURL.LoginUrl);
        /// Gets verification code from mozilla-vpn:// redirect after the user logs in through the browser.
        /// <param name="code">Verification code from the custom URL protocol redirect after logging in.</param>
        public void VerifyUserLogin(string code)
            // Make the POST request to the verify endpoint
            ApiRequest api = new ApiRequest(string.Empty, $"{ProductConstants.BaseUrl}{ApiVersion}/vpn/login/verify", RestSharp.Method.POST);

            // Get code_verifier that was generated in the login request
            string code_verifier = File.ReadAllText(Path.Combine(codeVerifierPath, codeVerifierFile));

            // Create a new dictionary to hold the code and code_verifier.
            Dictionary<string, string> postBody = new Dictionary<string, string>();
            postBody.Add("code", code);
            postBody.Add("code_verifier", code_verifier);
            api.AddPostBody(postBody);

            // Execute the request
            var response = api.SendRequest();

            if (response == null || response.StatusCode != System.Net.HttpStatusCode.OK)
                ErrorHandling.ErrorHandler.Handle(new ErrorHandling.UserFacingMessage("toast-login-url-retrieval-error"), ErrorHandling.UserFacingErrorType.None, ErrorHandling.LogLevel.Error);
                return;
            }

            // Check login credentials, add the session info to the settings, and add a new device
            Manager.Account.ProcessLogin(response.Content);
        /// Generates a Base64 encoded string based on a SHA256 hash with the string it's given.
        /// <param name="text">The text that needs to be converted to a SHA256 hash.</param>
        /// <returns>A Base64 encoded SHA256 hash.</returns>
        private static string GetHashSha256(string text)
            SHA256Managed hashTool = new SHA256Managed();
            byte[] phraseAsByte = Encoding.UTF8.GetBytes(string.Concat(text));
            byte[] encryptedBytes = hashTool.ComputeHash(phraseAsByte);
            hashTool.Clear();
            string value = Convert.ToBase64String(encryptedBytes);
            return value;
        }
        /// <summary>
        /// Generates a random cryptographic string to use in a PKCE auth request and response.
        /// </summary>
        /// <param name="length">The number of characters that should be in the string.</param>
        /// <param name="chars">The acceptable characters that can be in the string.</param>
        /// <returns>A cryptographic string.</returns>
        private static string GetUniqueToken(int length, string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890-_")
        {
            using (RNGCryptoServiceProvider crypto = new RNGCryptoServiceProvider())
            {
                byte[] data = new byte[length];
                // If chars.Length isn't a power of 2 then there is a bias if we simply use the modulus operator. The first characters of chars will be more probable than the last ones.
                // buffer used if we encounter an unusable random byte. We will regenerate it in this buffer.
                byte[] buffer = null;
                // Maximum random number that can be used without introducing a bias
                int maxRandom = byte.MaxValue - ((byte.MaxValue + 1) % chars.Length);
                crypto.GetBytes(data);

                char[] result = new char[length];

                for (int i = 0; i < length; i++)
                {
                    byte value = data[i];

                    while (value > maxRandom)
                        if (buffer == null)
                            buffer = new byte[1];

                        crypto.GetBytes(buffer);
                        value = buffer[0];
                    }

                    result[i] = chars[value % chars.Length];
                return new string(result);
                if (!login.StartLogin())
            var api = new ApiRequest(Manager.Account.Config.FxALogin.Token, $"{ProductConstants.BaseUrl}{Account.OriginalApiVersion}/vpn/servers", Method.GET);
using System.Collections.Generic;
using System.Diagnostics;
    internal class Entry : Application
            List<Process> processes = Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).ToList();

            if (args.Count() == 1 && !args[0].Contains("mozilla-vpn:"))
            if (processes.Count > 1)
                processes.Sort((x, y) => DateTime.Compare(y.StartTime, x.StartTime));

                if (processes.Count > 2)
                    for (int i = 1; i <= processes.Count - 2; i++)
                    {
                        processes[i].Kill();
                    }
                // Run this if the app is opened via the custom URL protocol with a code
                if (args.Count() == 1 && args[0].Contains("mozilla-vpn:"))
                {
                    // Get the code string that's returned from the server to be used in the verification request
                    Manager.Initialize(args.First());
                    VerifyUser(args.First());
                }
                else
                {
                    Manager.Initialize();
                }

        /// <summary>
        /// Starts the WireGuard tunnel service.
        /// </summary>
        /// <param name="args">
        /// Argument 0: "tunnel" keyword
        /// Argument 1: Path to the configuration file.
        /// </param>
        private static void VerifyUser(string args)
        {
            try
            {
                string code = args.Substring(args.IndexOf("code=") + 5);

                if (code != null && code.Length >= 44)
                {
                    FxA.Login verifyUser = new FxA.Login();

                    verifyUser.VerifyUserLogin(code);
                }
            }
            catch (Exception e)
            {
                ErrorHandling.ErrorHandler.Handle(e, ErrorHandling.LogLevel.Error);
                Environment.Exit(1);
            }
        }
        /// <param name="code">Optional verification code.</param>
        public static void Initialize(string code = "")
            InitializeViewModels(code);
        /// <param name="code">Optional verification code.</param>
        public static void InitializeViewModels(string code = "")
            MainWindowViewModel = new ViewModels.MainWindowViewModel(code);
            VersionUpdater.StopTask();
            ToastManager.StopThread();
            FxAUrl = string.Concat(BaseUrl, "/api/v2");
            FxALoginUrl = string.Concat(FxAUrl, "/vpn/login/windows");
                        <!-- Connection unstable -->
devices-remove-popup-content = Please confirm you would like to remove {$deviceName}. You will need to open the VPN client on the removed device to clear your network settings and access the internet again.
                        <CheckBox Margin="18" Content="{Binding Path=[settings-auto-launch]}" Click="RunOnStartup_Click" Visibility="Hidden">
        /// <param name="code">Optional verification code.</param>
        public MainWindowViewModel(string code)
            if (code.Length >= 44)
            {
                InitialViewFrameSourceType = typeof(UI.OnboardingView5);
            }
            else if (Manager.Account.LoginState == FxA.LoginState.LoggedIn && currentDevice != null)
        <requestedExecutionLevel level="highestAvailable" uiAccess="false" />
        <!--<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />-->
  <package id="Microsoft.IdentityModel.Logging" version="6.7.1" targetFramework="net472" />
  <package id="Microsoft.IdentityModel.Tokens" version="6.7.1" targetFramework="net472" />