public async Task PerformAuthenticationAsync(Action activateCallback)
        {
            // Clear any previous tokens
            this.AccessToken  = null;
            this.RefreshToken = null;

            // Generates state and PKCE values.
            string state         = OAuthHelpers.RandomDataBase64url(32);
            string codeVerifier  = OAuthHelpers.RandomDataBase64url(32);
            string codeChallenge = OAuthHelpers.Base64urlencodeNoPadding(OAuthHelpers.Sha256(codeVerifier));

            // Creates a redirect URI using an available port on the loopback address.
            string redirectURI = string.Format("http://{0}:{1}/", IPAddress.Loopback, OAuthHelpers.GetRandomUnusedPort());

            Output("redirect URI: " + redirectURI);

            // Creates an HttpListener to listen for requests on that redirect URI.
            var http = new HttpListener();

            http.Prefixes.Add(redirectURI);
            Output("Listening..");
            http.Start();

            // Creates the OAuth 2.0 authorization request.
            string authorizationRequest = string.Format("{0}?response_type=code&scope=openid%20profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
                                                        AuthorizationEndpoint,
                                                        System.Uri.EscapeDataString(redirectURI),
                                                        ClientID,
                                                        state,
                                                        codeChallenge,
                                                        CodeChallengeMethod);

            // Opens request in the browser.
            System.Diagnostics.Process.Start(authorizationRequest);

            // Waits for the OAuth authorization response.
            var context = await http.GetContextAsync();

            // Brings this app back to the foreground.
            activateCallback?.Invoke();

            // Sends an HTTP response to the browser.
            var response = context.Response;

            var buffer = Encoding.UTF8.GetBytes(ResponseString);

            response.ContentLength64 = buffer.Length;
            var  responseOutput = response.OutputStream;
            Task responseTask   = responseOutput.WriteAsync(buffer, 0, buffer.Length).ContinueWith((task) =>
            {
                responseOutput.Close();
                http.Stop();
                Console.WriteLine("HTTP server stopped.");
            });

            // Checks for errors.
            if (context.Request.QueryString.Get("error") != null)
            {
                throw new OAuthException(string.Format("OAuth authorization error: {0}.", context.Request.QueryString.Get("error")));
            }

            if (context.Request.QueryString.Get("code") == null ||
                context.Request.QueryString.Get("state") == null)
            {
                throw new OAuthException("Malformed authorization response. " + context.Request.QueryString);
            }

            // extracts the code
            var code           = context.Request.QueryString.Get("code");
            var incoming_state = context.Request.QueryString.Get("state");

            // Compares the receieved state to the expected value, to ensure that
            // this app made the request which resulted in authorization.
            if (incoming_state != state)
            {
                throw new OAuthException(string.Format("Received request with invalid state ({0})", incoming_state));
            }

            Output("Authorization code: " + code);

            // Starts the code exchange at the Token Endpoint.
            await PerformCodeExchange(code, codeVerifier, redirectURI);
        }