/// <summary>
        /// Attempt to refresh the access token, return a boolean indicating success (true)/failure (false), and
        /// include the response code and body from the Authorization Server in the event of failure
        /// </summary>
        /// <param name="errMsg"></param>
        /// <param name="statusCode"></param>
        /// <returns></returns>
        bool RefreshAccessToken(out string errMsg, out int statusCode)
        {
            using (var client = new HttpsClient())
            {
                CrestronConsole.PrintLine("Attempting to refresh Access Token...");
                HttpsClientRequest req = new HttpsClientRequest();
                req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
                req.Url         = new UrlParser(TokenEndpoint);
                HttpsHeaders headers = new HttpsHeaders();
                // Auth0's token endpoint expects all the necessary information to be
                // placed in the entity-body of the request
                headers.SetHeaderValue("Content-Type", "application/x-www-form-urlencoded");
                req.ContentString = BuildQueryString(
                    new NameValueCollection
                {
                    { "grant_type", "refresh_token" },
                    { "client_id", ClientID },
                    { "client_secret", ClientSecret },
                    { "refresh_token", RefreshToken }
                });

                // Always set the Content-Length of your POST request to indicate the length of the body,
                // or else the Content-Length will be set to 0 by default!
                headers.SetHeaderValue("Content-Length", req.ContentString.Length.ToString());
                req.Header = headers;

                // Send the POST request to the token endpoint and wait for a response...
                HttpsClientResponse tokenResponse = client.Dispatch(req);
                if (tokenResponse.Code >= 200 && tokenResponse.Code < 300)
                {
                    // Parse JSON response and securely store the token
                    JObject resBody = JsonConvert.DeserializeObject <JObject>(tokenResponse.ContentString);
                    accessToken = (string)resBody["access_token"];
                    CrestronConsole.PrintLine("Received a new \"{0}\" Access Token. It expires in {1} hours",
                                              (string)resBody["token_type"],
                                              (int)resBody["expires_in"] / 60.0 / 60.0);
                    // Client is now authorized to access the protected resource again
                    hasAccessToken = true;
                    errMsg         = "";
                    statusCode     = tokenResponse.Code;
                    return(true);
                }
                else
                {
                    CrestronConsole.PrintLine("Refresh Failed");
                    errMsg     = tokenResponse.ContentString;
                    statusCode = tokenResponse.Code;
                    return(false);
                }
            }
        }
        public void ProcessRequest(HttpCwsContext context)
        {
            try
            {
                Uri    requestUri = context.Request.Url;
                string method     = context.Request.HttpMethod;
                string path       = context.Request.Path.ToLower();
                switch (path)
                {
                case "/cws/":
                    if (method == "GET" || method == "HEAD")
                    {
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = "OK";
                        context.Response.ContentType       = "text/html";
                        // include the body for the GET request, but not HEAD
                        if (method == "GET")
                        {
                            context.Response.Write(CreateHtmlBody(startHtml,
                                                                  // Use the current state to decide whether to enable the buttons or not
                                                                  new NameValueCollection()
                            {
                                { "enable",
                                  Registered ? "" : "disabled" },
                                { "enable-forget-access",
                                  (Registered && hasAccessToken) ? "" : "disabled" },
                                { "enable-forget-refresh",
                                  (Registered && HasRefreshToken) ? "" : "disabled" }
                            }),
                                                   false);
                        }
                        context.Response.End();
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                case "/cws/register":
                    if (method == "GET" || method == "HEAD")
                    {
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = "OK";
                        context.Response.ContentType       = "text/html";
                        // include the body for the GET request, but not HEAD
                        if (method == "GET")
                        {
                            string body = Registered ? File.ReadToEnd(registeredHtml, Encoding.UTF8) :
                                          File.ReadToEnd(unregisteredHtml, Encoding.UTF8);

                            body = CreateHtmlBody(registerHtml,
                                                  new NameValueCollection()
                            {
                                { "register", body }
                            });
                            if (!Registered)
                            {
                                // Make two additional variable evaluations on the "unregistered" HTML page
                                string scheme = context.Request.Url.Scheme;
                                body = body.Replace("{@callbackUrl}", CallbackUrl);
                                body = body.Replace("{@httpsAlert}", (scheme == "https") ? "" :
                                                    "<p>WARNING: It looks like you got here via http, not https."
                                                    + " Check that SSL is enabled"
                                                    + " on the control system. Don't submit anything until it is!</p>");
                            }
                            context.Response.Write(body, false);
                        }
                        context.Response.End();
                    }
                    else if (method == "POST")
                    {
                        // Get the entity-body, which should contain the domain, client_id, and client_secret
                        // parameters
                        if (context.Request.ContentType != "application/x-www-form-urlencoded")
                        {
                            SendError(context, 415, "Content-Type must be application/x-www-form-urlencoded");
                            return;
                        }
                        // POST requests from HTML forms put their parameters in the request body
                        // using the application/x-www-form-urlencoded content type. An example can be found
                        // at https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
                        string requestBody;
                        // Parse the entity-body of the request and check for correctness
                        using (StreamReader sr = new StreamReader(context.Request.InputStream))
                        {
                            requestBody = sr.ReadToEnd();
                        }
                        NameValueCollection keyValuePairs = ParseQueryParams(requestBody);

                        string[] keys = keyValuePairs.AllKeys;

                        // Handler for the "Unregister and Return to Home Page" button
                        if (keys.Contains("unregister"))
                        {
                            Registered = false;
                            context.Response.Redirect("/cws/", true);
                            return;
                        }

                        // Check that all necessary registration fields (client_id, client_secret, and domain)
                        // have been provided
                        if (String.IsNullOrEmpty(keyValuePairs["domain"]) ||
                            String.IsNullOrEmpty(keyValuePairs["client_id"]) ||
                            String.IsNullOrEmpty(keyValuePairs["client_secret"]))
                        {
                            // Error, bad request
                            SendError(context, 400, "Registration submission is missing a necessary field");
                            return;
                        }
                        // Save the values provided to the datastore
                        Domain       = keyValuePairs["domain"];
                        ClientID     = keyValuePairs["client_id"];
                        ClientSecret = keyValuePairs["client_secret"];

                        // Send OK response
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = statusDescriptions[200];
                        context.Response.Write(CreateHtmlBody(regsuccessHtml,
                                                              new NameValueCollection()
                        {
                            { "domain", Domain },
                            { "client_id", ClientID }
                        }), true);

                        // Now the client is registered with the Authorization Server
                        Registered = true;
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD", "POST");
                    }
                    break;

                case "/cws/authorize":
                    if (!Registered)
                    {
                        SendError(context, 404, "The client must be registered with the Auth0 Authorization server" +
                                  " before it can get authorized");
                        return;
                    }
                    if (method == "GET" || method == "HEAD")
                    {
                        // First, discard the old set of tokens
                        accessToken     = Empty;
                        hasAccessToken  = false;
                        RefreshToken    = Empty;
                        HasRefreshToken = false;

                        // Generate a random state parameter, which the callback URL will check for
                        // consistency
                        stateString = Guid.NewGuid().ToString("N");
                        NameValueCollection queries =
                            new NameValueCollection()
                        {
                            // necessary scopes to get full user info and to get a Refresh Token
                            { "scope", "offline_access%20openid%20email%20profile" },
                            // Authorization code flow
                            { "response_type", "code" },
                            { "client_id", ClientID },
                            { "redirect_uri", CallbackUrl },
                            { "state", stateString },
                            // Always show the login and consent pages upon redirecting
                            { "prompt", "login%20consent" }
                        };

                        context.Response.StatusCode        = 302;
                        context.Response.StatusDescription = statusDescriptions[302];
                        // Contruct the redirect URL and send the user to Auth0's authorization endpoint
                        context.Response.AppendHeader("Location", BuildAuthorizationUrl(queries));
                        context.Response.End();
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                case "/cws/callback":
                    if (method == "HEAD" || method == "GET")
                    {
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = statusDescriptions[200];
                        if (method == "GET")
                        {
                            // Check if there's a query string
                            if (context.Request.Url.Query == null ||
                                context.Request.Url.Query == "")
                            {
                                SendError(context, 400, "No Query parameters provided");
                                return;
                            }
                            NameValueCollection queryParams = null;
                            // Parse query string and check state
                            try
                            {
                                queryParams = ParseQueryParams(context.Request.Url.Query);
                            }
                            catch (Exception e)
                            {
                                SendError(context, 400, "Query string parsing error: " + e.Message);
                                return;
                            }
                            if (queryParams["state"] != stateString)
                            {
                                CrestronConsole.PrintLine("State parameter mismatch: expected " +
                                                          stateString + " but got " + queryParams["state"]);
                                SendError(context, 400, "State parameter did not match.");
                                return;
                            }
                            authorizationCode = queryParams["code"];

                            if (String.IsNullOrEmpty(authorizationCode))
                            {
                                // Authorization was denied by the user
                                SendError(context, 400,
                                          "Authorization was denied. The OAuth Client has no Access or Refresh token now");
                                return;
                            }

                            // Using an HttpsClient, exchange the received authorization code for an Access Token
                            using (HttpsClient client = new HttpsClient())
                            {
                                HttpsClientRequest req = new HttpsClientRequest();
                                req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Post;
                                req.Url         = new UrlParser(TokenEndpoint);
                                CrestronConsole.PrintLine("GETting {0}", req.Url);
                                HttpsHeaders headers = new HttpsHeaders();
                                // Auth0's token endpoint expects all the necessary information to be
                                // placed in the entity-body of the request
                                headers.SetHeaderValue("Content-Type", "application/x-www-form-urlencoded");

                                req.ContentString = BuildQueryString(
                                    new NameValueCollection
                                {
                                    { "grant_type", "authorization_code" },
                                    { "client_id", ClientID },
                                    { "client_secret", ClientSecret },
                                    { "code", authorizationCode },
                                    { "redirect_uri", CallbackUrl }
                                });

                                // Always set the Content-Length of your POST request to indicate the length of the body,
                                // or else the Content-Length will be set to 0 by default!
                                headers.SetHeaderValue("Content-Length", req.ContentString.Length.ToString());
                                req.Header = headers;

                                // Send the POST request to the token endpoint and wait for a response...
                                HttpsClientResponse tokenResponse = client.Dispatch(req);
                                if (tokenResponse.Code >= 200 && tokenResponse.Code < 300)
                                {
                                    // Parse JSON response and securely store the tokens
                                    JObject resBody = JsonConvert.DeserializeObject <JObject>(tokenResponse.ContentString);
                                    accessToken     = (string)resBody["access_token"];
                                    hasAccessToken  = true;
                                    RefreshToken    = (string)resBody["refresh_token"];
                                    HasRefreshToken = true;
                                    CrestronConsole.PrintLine("Received \"{0}\" Access/Refresh Tokens. They expire in {1} hours",
                                                              (string)resBody["token_type"],
                                                              (int)resBody["expires_in"] / 60.0 / 60.0);
                                    // Respond
                                    context.Response.Write(CreateHtmlBody(authsuccessHtml, null), false);
                                }
                                else
                                {
                                    SendError(context, tokenResponse.Code,
                                              "Could not get access/refresh tokens. Token endpoint returned code " +
                                              "<strong>" + tokenResponse.Code + "</strong>" +
                                              "<br /><br />Error Message:<br /><br /><strong>"
                                              + tokenResponse.ContentString + "</strong>");
                                    return;
                                }
                            }
                        }
                        context.Response.End();
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                case "/cws/test":
                    if (method == "GET" || method == "HEAD")
                    {
                        if (method == "GET")
                        {
                            if (!Registered)
                            {
                                SendError(context, 404, "The client must be registered before it can try to access" +
                                          " a protected resource");
                                return;
                            }

                            // Send a GET request to the protected resource
                            using (HttpsClient client = new HttpsClient())
                            {
                                HttpsClientRequest req = new HttpsClientRequest();
                                req.RequestType = Crestron.SimplSharp.Net.Https.RequestType.Get;
                                req.Url         = new UrlParser(ResourceEndpoint);
                                CrestronConsole.PrintLine("GETting {0}", req.Url);
                                // Put the Access Token in the Authorization header, if it's present
                                if (hasAccessToken)
                                {
                                    req.Header.SetHeaderValue("Authorization", "Bearer " + accessToken);
                                }
                                var res = client.Dispatch(req);
                                if (res.Code >= 200 && res.Code < 300)
                                {
                                    JObject resBody =
                                        JsonConvert.DeserializeObject <JObject>(res.ContentString);
                                    string name, email, picUrl;
                                    bool   verified;
                                    // Try to get the username from any of these three JSON properties,
                                    // if they are defined. If none of them are defined, put "undefined" as a
                                    // placeholder name
                                    name = (string)resBody["given_name"] ??
                                           (string)resBody["nickname"] ??
                                           (string)resBody["email"] ??
                                           "undefined";
                                    email    = (string)resBody["email"] ?? "undefined";
                                    picUrl   = (string)resBody["picture"] ?? "undefined";
                                    verified = (bool?)resBody["email_verified"] ?? false;

                                    context.Response.Write(CreateHtmlBody(testHtml,
                                                                          new NameValueCollection()
                                    {
                                        { "name", name },
                                        { "email", email },
                                        { "verified", "<strong>" + (verified ? "verified" : "not verified")
                                          + "</strong>" },
                                        { "pic_url", picUrl },
                                    }), false);
                                }
                                else
                                {
                                    // If RefreshAccessToken succeeds, accessToken will contain the new token
                                    // If not, display the body of the error response returned from the
                                    // authorization server
                                    string errMsg;
                                    int    errCode;
                                    if (RefreshAccessToken(out errMsg, out errCode))
                                    {
                                        CrestronConsole.PrintLine("Successfully refreshed the Access Token." +
                                                                  " Redirecting user to /cws/Test...");
                                        context.Response.Redirect("/cws/Test", true);
                                        return;
                                    }
                                    else
                                    {
                                        SendError(context, errCode,
                                                  "Failed to refresh Access Token." +
                                                  " Resource endpoint returned code <strong>" + errCode + "</strong>" +
                                                  "<br /><br />Error Message:<br /><br /><strong>" + errMsg + "</strong>");
                                        return;
                                    }
                                }
                                context.Response.End();
                            }
                        }
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                case "/cws/forgetaccess":
                    if (method == "GET" || method == "HEAD")
                    {
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = statusDescriptions[200];
                        if (method == "GET")
                        {
                            if (hasAccessToken)
                            {
                                accessToken    = Empty;
                                hasAccessToken = false;
                                context.Response.Write(CreateHtmlBody(tokenDisposedHtml,
                                                                      new NameValueCollection
                                {
                                    { "token_kind", "Access" }
                                }), false);
                            }
                            else
                            {
                                SendError(context, 404, "There is currently no Access Token to be disposed of");
                            }
                        }
                        context.Response.End();
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                case "/cws/forgetrefresh":
                    if (method == "GET" || method == "HEAD")
                    {
                        context.Response.StatusCode        = 200;
                        context.Response.StatusDescription = statusDescriptions[200];
                        if (method == "GET")
                        {
                            if (HasRefreshToken)
                            {
                                RefreshToken    = Empty;
                                HasRefreshToken = false;
                                context.Response.Write(CreateHtmlBody(tokenDisposedHtml,
                                                                      new NameValueCollection
                                {
                                    { "token_kind", "Refresh" }
                                }), false);
                            }
                            else
                            {
                                SendError(context, 404, "There is currently no Refresh Token to be disposed of");
                            }
                        }
                        context.Response.End();
                    }
                    else
                    {
                        SendError(context, 405, context.Request.HttpMethod + " not allowed", "GET", "HEAD");
                    }
                    break;

                default:
                    SendError(context, 404, path + " not found");
                    break;
                }
            }
            catch (Exception e)
            {
                CrestronConsole.PrintLine("Error in ProcessRequest(): {0}", e);
                try
                {
                    SendError(context, 500, "An error occurred in the CWS server: " + e.Message);
                    CrestronConsole.PrintLine("Sent " + context.Response.StatusCode +
                                              " response to " + context.Request.UserHostName);
                }
                catch (Exception ex)
                {
                    CrestronConsole.PrintLine("Could not send Error response: {0}", ex);
                }
            }
            finally
            {
                CrestronConsole.PrintLine("Sent " + context.Response.StatusCode +
                                          " response to " + context.Request.UserHostName);
            }
        }