protected override async Task <AuthenticationTicket> CreateTicketAsync(ClaimsIdentity identity, AuthenticationProperties properties, OAuthTokenResponse tokens)
        {
            var address = QueryHelpers.AddQueryString(Options.UserInformationEndpoint, new Dictionary <string, string>
            {
                ["access_token"] = tokens.AccessToken,
                ["openid"]       = tokens.Response.GetRootString("openid")
            });

            var response = await Backchannel.GetAsync(address);

            if (!response.IsSuccessStatusCode)
            {
                Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
                                "returned a {Status} response with the following payload: {Headers} {Body}.",
                                /* Status: */ response.StatusCode,
                                /* Headers: */ response.Headers.ToString(),
                                /* Body: */ await response.Content.ReadAsStringAsync());

                throw new HttpRequestException("An error occurred while retrieving user information.");
            }

            var payload = JsonDocument.Parse(await response.Content.ReadAsStringAsync());

            if (!string.IsNullOrEmpty(payload.GetRootString("errcode")))
            {
                Logger.LogError("An error occurred while retrieving the user profile: the remote server " +
                                "returned a {Status} response with the following payload: {Headers} {Body}.",
                                /* Status: */ response.StatusCode,
                                /* Headers: */ response.Headers.ToString(),
                                /* Body: */ await response.Content.ReadAsStringAsync());

                throw new HttpRequestException("An error occurred while retrieving user information.");
            }

            var context = new OAuthCreatingTicketContext(new ClaimsPrincipal(identity), properties, Context, Scheme, Options, Backchannel, tokens, payload.RootElement);

            context.RunClaimActions();

            await Events.CreatingTicket(context);

            // TODO: 此处通过唯一的 CorrelationId, 将 properties生成的State缓存删除
            var state = Request.Query["state"];

            var stateCacheKey = WeChatAuthenticationStateCacheItem.CalculateCacheKey(state, null);
            await Cache.RemoveAsync(stateCacheKey, token : Context.RequestAborted);

            return(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
        }
        protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
        {
            await base.HandleChallengeAsync(properties);

            // TODO: 此处已经生成唯一的 CorrelationId, 可以借此将 properties生成State之后再进行缓存
            var state = properties.Items[".xsrf"];

            var stateToken    = Options.StateDataFormat.Protect(properties);
            var stateCacheKey = WeChatAuthenticationStateCacheItem.CalculateCacheKey(state, null);

            await Cache
            .SetAsync(
                stateCacheKey,
                new WeChatAuthenticationStateCacheItem(stateToken),
                new DistributedCacheEntryOptions
            {
                AbsoluteExpiration = Clock.UtcNow.AddMinutes(2)         // TODO: 设定2分钟过期?
            },
                token : Context.RequestAborted);
        }
        protected override async Task <HandleRequestResult> HandleRemoteAuthenticateAsync()
        {
            var query = Request.Query;

            // TODO: 此处借用唯一的 CorrelationId, 将 properties生成的State缓存取出,进行解密
            var state = query["state"];

            var stateCacheKey  = WeChatAuthenticationStateCacheItem.CalculateCacheKey(state, null);
            var stateCacheItem = await Cache.GetAsync(stateCacheKey, token : Context.RequestAborted);

            var properties = Options.StateDataFormat.Unprotect(stateCacheItem.State);

            if (properties == null)
            {
                return(HandleRequestResult.Fail("The oauth state was missing or invalid."));
            }

            // OAuth2 10.12 CSRF
            if (!ValidateCorrelationId(properties))
            {
                return(HandleRequestResult.Fail("Correlation failed.", properties));
            }

            var error = query["error"];

            if (!StringValues.IsNullOrEmpty(error))
            {
                // Note: access_denied errors are special protocol errors indicating the user didn't
                // approve the authorization demand requested by the remote authorization server.
                // Since it's a frequent scenario (that is not caused by incorrect configuration),
                // denied errors are handled differently using HandleAccessDeniedErrorAsync().
                // Visit https://tools.ietf.org/html/rfc6749#section-4.1.2.1 for more information.
                var errorDescription = query["error_description"];
                var errorUri         = query["error_uri"];
                if (StringValues.Equals(error, "access_denied"))
                {
                    var result = await HandleAccessDeniedErrorAsync(properties);

                    if (!result.None)
                    {
                        return(result);
                    }
                    var deniedEx = new Exception("Access was denied by the resource owner or by the remote server.");
                    deniedEx.Data["error"]             = error.ToString();
                    deniedEx.Data["error_description"] = errorDescription.ToString();
                    deniedEx.Data["error_uri"]         = errorUri.ToString();

                    return(HandleRequestResult.Fail(deniedEx, properties));
                }

                var failureMessage = new StringBuilder();
                failureMessage.Append(error);
                if (!StringValues.IsNullOrEmpty(errorDescription))
                {
                    failureMessage.Append(";Description=").Append(errorDescription);
                }
                if (!StringValues.IsNullOrEmpty(errorUri))
                {
                    failureMessage.Append(";Uri=").Append(errorUri);
                }

                var ex = new Exception(failureMessage.ToString());
                ex.Data["error"]             = error.ToString();
                ex.Data["error_description"] = errorDescription.ToString();
                ex.Data["error_uri"]         = errorUri.ToString();

                return(HandleRequestResult.Fail(ex, properties));
            }

            var code = query["code"];

            if (StringValues.IsNullOrEmpty(code))
            {
                return(HandleRequestResult.Fail("Code was not found.", properties));
            }

            var codeExchangeContext = new OAuthCodeExchangeContext(properties, code, BuildRedirectUri(Options.CallbackPath));

            using var tokens = await ExchangeCodeAsync(codeExchangeContext);

            if (tokens.Error != null)
            {
                return(HandleRequestResult.Fail(tokens.Error, properties));
            }

            if (string.IsNullOrEmpty(tokens.AccessToken))
            {
                return(HandleRequestResult.Fail("Failed to retrieve access token.", properties));
            }

            var identity = new ClaimsIdentity(ClaimsIssuer);

            if (Options.SaveTokens)
            {
                var authTokens = new List <AuthenticationToken>();

                authTokens.Add(new AuthenticationToken {
                    Name = "access_token", Value = tokens.AccessToken
                });
                if (!string.IsNullOrEmpty(tokens.RefreshToken))
                {
                    authTokens.Add(new AuthenticationToken {
                        Name = "refresh_token", Value = tokens.RefreshToken
                    });
                }

                if (!string.IsNullOrEmpty(tokens.TokenType))
                {
                    authTokens.Add(new AuthenticationToken {
                        Name = "token_type", Value = tokens.TokenType
                    });
                }

                if (!string.IsNullOrEmpty(tokens.ExpiresIn))
                {
                    int value;
                    if (int.TryParse(tokens.ExpiresIn, NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
                    {
                        // https://www.w3.org/TR/xmlschema-2/#dateTime
                        // https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx
                        var expiresAt = Clock.UtcNow + TimeSpan.FromSeconds(value);
                        authTokens.Add(new AuthenticationToken
                        {
                            Name  = "expires_at",
                            Value = expiresAt.ToString("o", CultureInfo.InvariantCulture)
                        });
                    }
                }

                properties.StoreTokens(authTokens);
            }

            var ticket = await CreateTicketAsync(identity, properties, tokens);

            if (ticket != null)
            {
                return(HandleRequestResult.Success(ticket));
            }
            else
            {
                return(HandleRequestResult.Fail("Failed to retrieve user information from remote server.", properties));
            }
        }