/// <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<Uri>.</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)); }