/// <summary>
        ///     Validates the state of the authentication.
        /// </summary>
        /// <param name="authState">State of the authentication.</param>
        /// <returns>OpenIdConnectAuthorizationState.</returns>
        /// <exception cref="AuthenticationException">
        ///     The authorization state has invalid data.
        ///     or
        ///     The authorization state has expired.
        /// </exception>
        private OpenIdConnectAuthorizationState ValidateAuthState(OpenIdConnectAuthorizationState authState)
        {
            // Validate fields
            if (authState.Timestamp <= 0 ||
                authState.IdentityProviderId <= 0 ||
                string.IsNullOrWhiteSpace(authState.RedirectUrl) ||
                string.IsNullOrWhiteSpace(authState.Nonce) ||
                authState.TenantId <= 0)
            {
                throw new AuthenticationException("The authorization state has invalid data.");
            }

            // Validate timestamp
            var timestamp    = new DateTime(authState.Timestamp, DateTimeKind.Utc);
            var maxValidTime = timestamp.AddHours(1);
            var nowUtc       = DateTime.UtcNow;

            if (nowUtc < timestamp || nowUtc > maxValidTime)
            {
                throw new AuthenticationException("The authorization state has expired.");
            }

            return(authState);
        }
        /// <summary>
        ///     Gets the authorization code request URL for the specified identity provider.
        /// </summary>
        /// <param name="idpLoginRequest">The oidc provider.</param>
        /// <param name="requestBaseUrl">The base url message.</param>
        /// <returns>Task&lt;Uri&gt;.</returns>
        /// <exception cref="System.ArgumentNullException">
        /// </exception>
        public async Task <Uri> GetAuthorizationCodeRequestUrl(IdentityProviderLoginRequest idpLoginRequest, Uri requestBaseUrl)
        {
            if (idpLoginRequest == null)
            {
                throw new ArgumentNullException(nameof(idpLoginRequest));
            }

            if (requestBaseUrl == null)
            {
                throw new ArgumentNullException(nameof(requestBaseUrl));
            }

            if (string.IsNullOrWhiteSpace(idpLoginRequest.Tenant))
            {
                throw new ArgumentException(@"The tenant is invalid.", nameof(idpLoginRequest));
            }

            if (idpLoginRequest.IdentityProviderId <= 0)
            {
                throw new ArgumentException(@"The identity provider is invalid.", nameof(idpLoginRequest));
            }

            if (string.IsNullOrWhiteSpace(idpLoginRequest.RedirectUrl))
            {
                throw new ArgumentException(@"The redirect url is invalid.", nameof(idpLoginRequest));
            }

            long   tenantId;
            long   oidcProviderId;
            string oidcConfigurationUrl;
            string oidcClientId;
            bool   alwaysPrompt;

            using (new SecurityBypassContext())
                using (new TenantAdministratorContext(idpLoginRequest.Tenant))
                {
                    var oidcIdentityProvider = ReadiNow.Model.Entity.Get <OidcIdentityProvider>(idpLoginRequest.IdentityProviderId, GetOidcProviderFieldsToLoad());

                    if (oidcIdentityProvider == null)
                    {
                        throw new AuthenticationException("The identity provider does not exist.");
                    }

                    ValidateOidcProviderFields(oidcIdentityProvider);

                    // Store any required entity model fields upfront.
                    // Any code running after the await statement may run a different thread
                    tenantId             = RequestContext.TenantId;
                    oidcProviderId       = oidcIdentityProvider.Id;
                    oidcClientId         = oidcIdentityProvider.OidcClientId;
                    oidcConfigurationUrl = oidcIdentityProvider.OidcIdentityProviderConfigurationUrl;
                    alwaysPrompt         = oidcIdentityProvider.OidcAlwaysPrompt ?? true;
                }

            OpenIdConnectConfiguration oidcConfig;

            try
            {
                // Get the configuration
                oidcConfig = await _configurationManager.GetIdentityProviderConfigurationAsync(oidcConfigurationUrl);
            }
            catch (Exception ex)
            {
                throw new OidcProviderInvalidConfigurationException(ex);
            }

            var oidcProtocolValidator = new OpenIdConnectProtocolValidator();

            // Create authorization state
            var authStateObject = new OpenIdConnectAuthorizationState
            {
                Timestamp          = DateTime.UtcNow.Ticks,
                RedirectUrl        = idpLoginRequest.RedirectUrl,
                IdentityProviderId = oidcProviderId,
                TenantId           = tenantId,
                Nonce = oidcProtocolValidator.GenerateNonce()
            };

            // Serialize and encrypt state
            var stateJson      = JSON.Serialize(authStateObject);
            var cryptoProvider = new EncodingCryptoProvider();
            var encryptedState = cryptoProvider.EncryptAndEncode(stateJson);

            // Create code request oidc message
            var oidcMessage = new OpenIdConnectMessage
            {
                ClientId      = oidcClientId,
                IssuerAddress = oidcConfig.AuthorizationEndpoint,
                RedirectUri   = GetOidcAuthResponseUri(requestBaseUrl, idpLoginRequest.Tenant),
                Scope         = "openid email",
                ResponseType  = "code",
                State         = encryptedState,
                Nonce         = authStateObject.Nonce,
                Prompt        = alwaysPrompt ? "login" : null
            };

            // Get request url
            return(new Uri(oidcMessage.CreateAuthenticationRequestUrl()));
        }
        /// <summary>
        ///     Processes the oidc authorization response.
        /// </summary>
        /// <param name="tenant"></param>
        /// <param name="code">The code.</param>
        /// <param name="authState">The authentication state.</param>
        /// <param name="requestBaseUrl">The request base url.</param>
        /// <param name="httpClient">The HTTP client.</param>
        /// <returns>Task.</returns>
        public async Task <OpenIdConnectRequestContextResult> ProcessOidcAuthorizationResponse(string tenant, string code, OpenIdConnectAuthorizationState authState, Uri requestBaseUrl,
                                                                                               IHttpClient httpClient)
        {
            if (string.IsNullOrWhiteSpace(tenant))
            {
                throw new ArgumentNullException(nameof(tenant));
            }

            if (string.IsNullOrWhiteSpace(code))
            {
                throw new ArgumentNullException(nameof(code));
            }

            if (authState == null)
            {
                throw new ArgumentNullException(nameof(authState));
            }

            if (requestBaseUrl == null)
            {
                throw new ArgumentNullException(nameof(requestBaseUrl));
            }

            if (httpClient == null)
            {
                throw new ArgumentNullException(nameof(httpClient));
            }

            ValidateAuthState(authState);

            long   identityProviderId;
            string oidcClientId;
            Guid?  oidcClientSecretSecureId;
            string oidcIdentityClaim;
            string oidcConfigurationUrl;

            using (new SecurityBypassContext())
                using (new TenantAdministratorContext(authState.TenantId))
                {
                    var requestContext = RequestContext.GetContext();
                    if (string.Compare(requestContext.Tenant.Name, tenant, StringComparison.OrdinalIgnoreCase) != 0)
                    {
                        throw new AuthenticationException("The tenant is invalid.");
                    }

                    var oidcIdentityProvider = ReadiNow.Model.Entity.Get <OidcIdentityProvider>(authState.IdentityProviderId, GetOidcProviderFieldsToLoad());

                    if (oidcIdentityProvider == null)
                    {
                        throw new AuthenticationException("The identity provider does not exist.");
                    }

                    ValidateOidcProviderFields(oidcIdentityProvider);

                    // Store any required entity model fields upfront.
                    // Any code running after the await statement may run a different thread
                    identityProviderId = oidcIdentityProvider.Id;
                    oidcClientId       = oidcIdentityProvider.OidcClientId;

                    oidcClientSecretSecureId = oidcIdentityProvider.OidcClientSecretSecureId;

                    oidcIdentityClaim    = oidcIdentityProvider.OidcUserIdentityClaim;
                    oidcConfigurationUrl = oidcIdentityProvider.OidcIdentityProviderConfigurationUrl;
                }

            OpenIdConnectConfiguration oidcConfig;

            try
            {
                oidcConfig = await _configurationManager.GetIdentityProviderConfigurationAsync(oidcConfigurationUrl);
            }
            catch (Exception ex)
            {
                throw new OidcProviderInvalidConfigurationException(ex);
            }

            JwtSecurityToken idToken = null;

            try
            {
                var authResponse = await GetAuthorizationTokens(tenant, code, oidcClientSecretSecureId, oidcClientId, requestBaseUrl, oidcConfig, httpClient);

                const int maxRetries = 2;
                int       retryCount = 0;

                while (retryCount <= maxRetries)
                {
                    try
                    {
                        if (retryCount > 0)
                        {
                            // Are retrying due to an error.
                            // Remove the config from the cache and get a fresh copy
                            _configurationManager.RemoveIdentityProviderConfiguration(oidcConfigurationUrl);
                            oidcConfig = await _configurationManager.GetIdentityProviderConfigurationAsync(oidcConfigurationUrl);
                        }

                        idToken = ValidateIdentityToken(authResponse.IdToken, oidcClientId, authState.Nonce, oidcConfig);
                        // Got the token, so break out of retry loop.
                        break;
                    }
                    catch
                    {
                        retryCount++;
                        if (retryCount >= maxRetries)
                        {
                            throw;
                        }
                    }
                }
            }
            catch
            {
                // Id provider may have cycled keys. Remove the configuration so that the next request gets updated config.
                _configurationManager.RemoveIdentityProviderConfiguration(oidcConfigurationUrl);
                throw;
            }

            return(GetRequestContextData(idToken, authState, identityProviderId, oidcIdentityClaim));
        }
        /// <summary>
        ///     Gets the request context data.
        /// </summary>
        /// <param name="idToken">The identifier token.</param>
        /// <param name="authState">State of the authentication.</param>
        /// <param name="identityProviderId">The identity provider identifier.</param>
        /// <param name="oidcIdentityClaim">The oidc identity claim.</param>
        /// <exception cref="System.ArgumentNullException"></exception>
        /// <exception cref="AuthenticationException">
        /// </exception>
        private OpenIdConnectRequestContextResult GetRequestContextData(JwtSecurityToken idToken, OpenIdConnectAuthorizationState authState, long identityProviderId,
                                                                        string oidcIdentityClaim)
        {
            if (idToken == null)
            {
                throw new ArgumentNullException(nameof(idToken));
            }

            // Get the claim from the id token
            var claim = idToken.Claims.FirstOrDefault(c => c.Type == oidcIdentityClaim);

            if (claim == null)
            {
                EventLog.Application.WriteError("The identity token does not contain claim {0}.", oidcIdentityClaim);
                throw new OidcProviderInvalidConfigurationException();
            }

            var claimValue = claim.Value;

            // Find the identity user for this tenant, provider and claim for this provider with the specified value
            var requestContextData = Factory.IdentityProviderRequestContextCache.GetRequestContextData(authState.TenantId, identityProviderId, claimValue, true);

            if (requestContextData == null)
            {
                using (new TenantAdministratorContext(authState.TenantId))
                {
                    AuditLogInstance.Get().OnLogon(false, claimValue, null);        // Didn't seem worth passing the agent information into the method to handle this edge case
                }
                throw new AuthenticationException("The request context could not be found. The user name may be incorrect or the account may be locked, disabled or expired.");
            }

            return(new OpenIdConnectRequestContextResult(requestContextData, claimValue));
        }