private static Task OnValidatePrincipal(
            CookieValidatePrincipalContext context, OpenIdAuthOptions authOptions)
        {
            // Don't need to worry about about unauth request.
            if (context.Principal?.Identity == null || !context.Principal.Identity.IsAuthenticated)
            {
                return(Task.CompletedTask);
            }

            var absoluteExpiry = DateTime.Parse(context.Properties.Items[".Token.expires_at"]).ToUniversalTime();

            // If the token is still not expired then don't need to reject the principal.
            if (absoluteExpiry > DateTime.UtcNow)
            {
                return(Task.CompletedTask);
            }

            // Token has expired, time to reject the Principal which will kick off the auth process again.
            var logger = context.HttpContext.RequestServices.GetRequiredService <ILogger <Startup> >();

            logger.LogInformation(
                $"Token has expired @ {absoluteExpiry:O} compared to current date time of {DateTime.UtcNow:O}");

            context.RejectPrincipal();

            return(Task.CompletedTask);
        }
        private static async Task <bool> RefreshAccessToken(
            CookieValidatePrincipalContext context, OpenIdAuthOptions authOptions)
        {
            // True = token refreshed. False = Token not refreshed and Principal will have to be rejected so the pipeline will kick off the login process.
            // Note that this function is not working as expected due to an issue with the context properties not updating correctly when setting the new
            // Expiry and Refresh token.

            if (context.Properties.Items.ContainsKey(".Token.refresh_token"))
            {
                var refreshToken = context.Properties.Items[".Token.refresh_token"];

                // Reference token.
                var discoveryCache = context.HttpContext.RequestServices.GetRequiredService <IDiscoveryCache>();

                var disco = await discoveryCache.GetAsync();

                if (disco.IsError)
                {
                    throw new Exception(disco.Error);
                }

                var client = context.HttpContext.RequestServices.GetRequiredService <IHttpClientFactory>()
                             .CreateClient();

                var refreshTokenResponse = await client.RequestRefreshTokenAsync(new RefreshTokenRequest
                {
                    Address      = disco.TokenEndpoint,
                    ClientId     = authOptions.ClientId,
                    ClientSecret = authOptions.ClientSecret,
                    RefreshToken = refreshToken
                });

                if (!refreshTokenResponse.IsError)
                {
                    var id =
                        await GetClaimsIdentity(
                            context.Scheme.Name, refreshTokenResponse.AccessToken, context.HttpContext, authOptions);

                    context.ReplacePrincipal(new ClaimsPrincipal(id));
                    context.Properties.Items[".Token.refresh_token"] = refreshTokenResponse.RefreshToken;
                    context.Properties.Items[".Token.expires_at"]    =
                        DateTime.UtcNow.AddSeconds(refreshTokenResponse.ExpiresIn).ToString("O");

                    return(true);
                }
            }

            return(false);
        }
        private static async Task SetupIdentityAndClaimsAndGa(
            TicketReceivedContext context, OpenIdAuthOptions authOptions)
        {
            var id = await GetClaimsIdentity(
                context.Scheme.Name,
                context.Properties.GetTokenValue("access_token"),
                context.HttpContext,
                authOptions,
                context.Principal.Claims?.ToArray());

            await RegisterExternalAccount(context, id);

            try
            {
                await context.HttpContext.ConfirmAccountAsync(
                    id,
                    context.Properties.GetTokenValue("access_token"));
            }
            catch (Exception)
            {
                // Not logging here as it's already logged elsewhere.

                context.HandleResponse();
                context.Response.Redirect("/error/authfailure");

                return;
            }

            context.Principal = new ClaimsPrincipal(id);

            // The following is for GA.
            var subjectIdClaim = id.Claims.FirstOrDefault(x => x.Type == "sub");

            if (subjectIdClaim != null)
            {
                var distributedCache =
                    context.HttpContext.RequestServices.GetRequiredService <IDistributedCache>();

                await distributedCache.SetAsync($"JUST-LOGGED-IN-{subjectIdClaim?.Value}",
                                                Encoding.UTF8.GetBytes("TRUE"),
                                                new DistributedCacheEntryOptions
                {
                    AbsoluteExpirationRelativeToNow = TimeSpan.FromSeconds(5)
                });
            }
        }
        private static async Task <ClaimsIdentity> GetClaimsIdentity(
            string schemeName,
            string accessToken,
            HttpContext context,
            OpenIdAuthOptions authOptions,
            Claim[] additionalClaims = null)
        {
            var id = new ClaimsIdentity(schemeName);

            if (string.Equals(authOptions.TokenType, TokenTypes.Jwt, StringComparison.CurrentCultureIgnoreCase))
            {
                // JWT token.

                var handler  = new JwtSecurityTokenHandler();
                var jwtToken = handler.ReadJwtToken(accessToken);

                id.AddClaims(jwtToken.Claims);
            }
            else
            {
                // Reference token.
                var discoveryCache = context.RequestServices.GetRequiredService <IDiscoveryCache>();

                var disco = await discoveryCache.GetAsync();

                if (disco.IsError)
                {
                    throw new Exception(disco.Error);
                }

                var client = context.RequestServices.GetRequiredService <IHttpClientFactory>()
                             .CreateClient();

                var introspectionResponse = await client.IntrospectTokenAsync(new TokenIntrospectionRequest
                {
                    Address      = disco.IntrospectionEndpoint,
                    ClientId     = authOptions.ScopeId,
                    ClientSecret = authOptions.ScopeSecret,

                    Token = accessToken
                });

                if (introspectionResponse.IsError)
                {
                    throw new Exception(introspectionResponse.Error);
                }

                if (!introspectionResponse.IsActive)
                {
                    throw new Exception("Obtained token is inactive.");
                }

                id.AddClaims(introspectionResponse.Claims);
            }

            if (additionalClaims != null && additionalClaims.Any())
            {
                id.AddClaims(additionalClaims);
            }

            return(id);
        }
        public static void AddOpenIdConnectAuth(
            this IServiceCollection services, OpenIdAuthOptions authOptions)
        {
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();

            services.AddAuthentication(options =>
            {
                options.DefaultScheme          = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie(options =>
            {
                // Surprisingly there are no DI for this!
                options.SessionStore = new RedisCacheTicketStore(new RedisCache(new RedisCacheOptions
                {
                    Configuration = authOptions.RedisConfiguration
                }));

                options.Cookie.SameSite = SameSiteMode.None;

                // This is so we can manually check if the Token is expired so we can re-new it or logout the user.
                options.Events.OnValidatePrincipal = context => OnValidatePrincipal(context, authOptions);
            })
            .AddOpenIdConnect(options =>
            {
                options.Authority = authOptions.AuthorityEndpoint;

                options.ClientId     = authOptions.ClientId;
                options.ClientSecret = authOptions.ClientSecret;

                options.ResponseType         = "code id_token";
                options.RequireHttpsMetadata = authOptions.RequireHttpsMetadata;

                // This sets the cookie expiry to match the ID Token if true. This is of no use so setting it to false.
                options.UseTokenLifetime = false;

                options.SaveTokens = true;

                foreach (var scope in authOptions.Scopes)
                {
                    options.Scope.Add(scope);
                }

                // Form post is more secure.
                options.AuthenticationMethod = OpenIdConnectRedirectBehavior.FormPost;
                options.RequireHttpsMetadata = options.RequireHttpsMetadata;

                if (options.Events == null)
                {
                    options.Events = new OpenIdConnectEvents();
                }

                options.Events.OnRemoteFailure              = HandleRemoteFailure;
                options.Events.OnAuthenticationFailed       = HandleAuthenticationFailure;
                options.Events.OnTicketReceived             = context => SetupIdentityAndClaimsAndGa(context, authOptions);
                options.Events.OnRedirectToIdentityProvider = SetupAdditionalProperties;

                options.SecurityTokenValidator = new JwtSecurityTokenHandler
                {
                    InboundClaimTypeMap = new Dictionary <string, string>()
                };

                options.TokenValidationParameters.NameClaimType = ClaimTypes.Name;
            });

            services.AddSingleton <IDiscoveryCache>(r =>
            {
                var factory = r.GetRequiredService <IHttpClientFactory>();
                return(new DiscoveryCache(authOptions.AuthorityEndpoint, () => factory.CreateClient(),
                                          new DiscoveryPolicy
                {
                    RequireHttps = authOptions.RequireHttpsMetadata,
                    AuthorityNameComparison = StringComparison.CurrentCultureIgnoreCase
                }));
            });
        }