private static void CheckOptions(IServiceCollection services, ClientOptions options)
        {
            if (options.IdentityClaimsBuilder == null)
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.IdentityClaimsBuilder)}");
            }
            if (string.IsNullOrWhiteSpace(options.ClientID))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.ClientID)}");
            }
            if (string.IsNullOrWhiteSpace(options.ClientSecret))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.ClientSecret)}");
            }
            if (string.IsNullOrWhiteSpace(options.AuthorizationEndpoint))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.AuthorizationEndpoint)}");
            }
            if (string.IsNullOrWhiteSpace(options.TokenEndpoint))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.TokenEndpoint)}");
            }
            if (string.IsNullOrWhiteSpace(options.SignInCallbackPath))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.SignInCallbackPath)}");
            }
            if (string.IsNullOrWhiteSpace(options.SignOutCallbackPath))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.SignOutCallbackPath)}");
            }
            if (string.IsNullOrWhiteSpace(options.SignOutPath))
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.SignOutPath)}");
            }

            // StateStore
            if (options.StateStoreFactory != null)
            {
                services.AddSingleton(options.StateStoreFactory);
            }
            else
            {
                throw new ArgumentNullException($"{nameof(options)}.{nameof(options.StateStoreFactory)}");
            }

            if (options.StateGeneratorFactory != null)
            {
                services.AddSingleton(options.StateGeneratorFactory);
            }
            else
            {
                // use default
                services.AddSingleton <IStateGenerator, DefaultStateGenerator>();
            }

            if (options.ClientServerFactory != null)
            {
                services.AddSingleton(options.ClientServerFactory);
            }
            else
            {
                // use default
                services.AddSingleton <IClientServer, DefaultClientServer>();
            }
        }
        public static IServiceCollection AddOAuth2Client(this IServiceCollection services, Action <ClientOptions> configOptions, ClientOptions options = null)
        {
            options = options ?? new ClientOptions();
            configOptions(options);
            CheckOptions(services, options);

            services.AddSingleton(options);

            services.AddHttpClient();
            services.AddHttpContextAccessor();
            services.AddTransient <IDataSerializer <TokenDTO>, JsonDataSerializer <TokenDTO> >();
            services.AddTransient <ISecureDataFormat <TokenDTO>, SecureDataFormat <TokenDTO> >();
            services.AddTransient(c =>
            {
                var dpp = c.GetService <IDataProtectionProvider>();
                return(dpp.CreateProtector(nameof(TokenDTO)));
            });
            services.AddTransient <ITokenDTOStore, HttpContextTokenDTOStore>();

            var sp = services.BuildServiceProvider();
            var httpClientFactory = sp.GetService <IHttpClientFactory>();
            var tokenDTOStore     = sp.GetService <ITokenDTOStore>();

            services.AddAuthentication(authOptions =>
            {
                authOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                authOptions.DefaultSignInScheme       = CookieAuthenticationDefaults.AuthenticationScheme;
                authOptions.DefaultChallengeScheme    = OAuthDefaults.DisplayName;
            })
            .AddCookie(o =>
            {
                //o.Events.OnSigningIn = context =>
                //{
                //    //context.Properties.IsPersistent = true;
                //    context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(14);
                //    //var expStr = context.Properties.GetTokenValue(OAuth2Consts.Form_RefreshToken);
                //    return Task.CompletedTask;
                //};
                if (options.AutoRefreshToken)
                {
                    o.Events.OnValidatePrincipal = x => ValidatePrincipal(x, httpClientFactory, tokenDTOStore, options);
                }
            })
            .AddOAuth <OAuthOptions, OAuth2Handler>(OAuthDefaults.DisplayName, o =>
            {
                foreach (var scope in options.Scopes)
                {
                    o.Scope.Add(scope);
                }
                o.ClientId              = options.ClientID;
                o.ClientSecret          = options.ClientSecret;
                o.AuthorizationEndpoint = options.AuthorizationEndpoint;
                o.TokenEndpoint         = options.TokenEndpoint;
                o.CallbackPath          = options.SignInCallbackPath;
                //o.SaveTokens = options.SaveTokens;    // Use customized token store
                o.UsePkce = options.UsePkce;

                o.Events.OnCreatingTicket = async context =>
                {
                    context.Principal = new ClaimsPrincipal(new ClaimsIdentity(OAuthDefaults.DisplayName, OAuth2Consts.Claim_Name, OAuth2Consts.Claim_Role));

                    // Save token to cookie, and return a json web token
                    var jwt = await tokenDTOStore.SaveTokenDTOAsync(context.TokenResponse.Response.ToJsonString());
                    if (jwt != null)
                    {
                        var claims = options.IdentityClaimsBuilder(jwt);
                        foreach (var claim in claims)
                        {
                            context.Identity.AddClaim(claim);
                        }
                    }
                };
            });

            return(services);
        }
        private static async Task ValidatePrincipal(CookieValidatePrincipalContext context, IHttpClientFactory httpClientFactory, ITokenDTOStore tokenDTOStore, ClientOptions options)
        {
            var tokenDTO = await tokenDTOStore.GetTokenDTOAsync();

            var jwt = tokenDTO.GetJwt();

            //if (!jwt.TryGetPayloadValue<long>(OAuth2Consts.Claim_AccessTokenExpire, out var exp))
            //{// expStr format invalid
            //    // reject principal
            //    context.RejectPrincipal();
            //    // sign user out
            //    await context.HttpContext.SignOutAsync();
            //    return;
            //}

            if (DateTimeOffset.UtcNow > jwt.ValidTo)
            {     // access token expired
                if (!string.IsNullOrWhiteSpace(tokenDTO.RefreshToken))
                { // refresh token exists
                    // send refresh token request
                    var httpClient       = httpClientFactory.CreateClient();
                    var refreshTokenResp = await httpClient.RequestRefreshTokenAsync(new RefreshTokenRequest
                    {
                        Address      = options.TokenEndpoint,
                        ClientId     = options.ClientID,
                        ClientSecret = options.ClientSecret,
                        RefreshToken = tokenDTO.RefreshToken,
                        Scope        = string.Join(OAuth2Consts.Seperator_Scope, options.Scopes)
                    });

                    if (!refreshTokenResp.IsError)
                    {// refresh success
                        await tokenDTOStore.SaveTokenDTOAsync(refreshTokenResp.Raw);

                        //context.Properties.UpdateTokenValue(OAuth2Consts.Token_Access, refreshTokenResp.AccessToken);
                        //context.Properties.UpdateTokenValue(OAuth2Consts.Token_Refresh, refreshTokenResp.RefreshToken);
                        //var expireAt = DateTimeOffset.UtcNow.AddSeconds(refreshTokenResp.ExpiresIn).ToString(OAuth2Consts.UtcTimesamp);
                        //context.Properties.UpdateTokenValue(OAuth2Consts.Token_ExpiresAt, expireAt);
                        //context.ShouldRenew = true;
                        return;
                    }
                }

                // reject principal
                context.RejectPrincipal();
                // sign user out
                await context.HttpContext.OAuth2SignOutAsync();
            }
        }