private OpenIdConnectProtocolException CreateOpenIdConnectProtocolException(OpenIdConnectMessage message) { var description = message.ErrorDescription ?? "error_description is null"; var errorUri = message.ErrorUri ?? "error_uri is null"; var errorMessage = string.Format( CultureInfo.InvariantCulture, Resources.Exception_OpenIdConnectMessageError, message.Error, description, errorUri); _logger.WriteError(errorMessage); var ex = new OpenIdConnectProtocolException(errorMessage); ex.Data["error"] = message.Error; ex.Data["error_description"] = description; ex.Data["error_uri"] = errorUri; return(ex); }
/// <summary> /// Invoked to process incoming authentication messages. /// </summary> /// <returns>An <see cref="AuthenticationTicket"/> if successful.</returns> protected override async Task <AuthenticationTicket> AuthenticateCoreAsync() { // Allow login to be constrained to a specific path. Need to make this runtime configurable. if (Options.CallbackPath.HasValue && Options.CallbackPath != (Request.PathBase + Request.Path)) { return(null); } OpenIdConnectMessage authorizationResponse = null; if (string.Equals(Request.Method, "GET", StringComparison.OrdinalIgnoreCase) && Request.Query.Any()) { authorizationResponse = new OpenIdConnectMessage(Request.Query.Select(pair => new KeyValuePair <string, string[]>(pair.Key, pair.Value))); // response_mode=query (explicit or not) and a response_type containing id_token // or token are not considered as a safe combination and MUST be rejected. // See http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#Security if (!string.IsNullOrEmpty(authorizationResponse.IdToken) || !string.IsNullOrEmpty(authorizationResponse.AccessToken)) { var invalidResponseEx = new OpenIdConnectProtocolException("An OpenID Connect response cannot contain an identity token or an access token when using response_mode=query"); _logger.WriteError("Exception occurred while processing message: ", invalidResponseEx); var authenticationFailedNotification = new AuthenticationFailedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { ProtocolMessage = authorizationResponse, Exception = invalidResponseEx }; await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); if (authenticationFailedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (authenticationFailedNotification.Skipped) { return(null); } throw invalidResponseEx; } } // assumption: if the ContentType is "application/x-www-form-urlencoded" it should be safe to read as it is small. else if (string.Equals(Request.Method, "POST", StringComparison.OrdinalIgnoreCase) && !string.IsNullOrWhiteSpace(Request.ContentType) // May have media/type; charset=utf-8, allow partial match. && Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase) && Request.Body.CanRead) { if (!Request.Body.CanSeek) { _logger.WriteVerbose("Buffering request body"); // Buffer in case this body was not meant for us. MemoryStream memoryStream = new MemoryStream(); await Request.Body.CopyToAsync(memoryStream); memoryStream.Seek(0, SeekOrigin.Begin); Request.Body = memoryStream; } IFormCollection form = await Request.ReadFormAsync(); Request.Body.Seek(0, SeekOrigin.Begin); // TODO: a delegate on OpenIdConnectAuthenticationOptions would allow for users to hook their own custom message. authorizationResponse = new OpenIdConnectMessage(form); } if (authorizationResponse == null) { return(null); } ExceptionDispatchInfo authFailedEx = null; try { var messageReceivedNotification = new MessageReceivedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { ProtocolMessage = authorizationResponse }; await Options.Notifications.MessageReceived(messageReceivedNotification); if (messageReceivedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (messageReceivedNotification.Skipped) { return(null); } // runtime always adds state, if we don't find it OR we failed to 'unprotect' it this is not a message we // should process. AuthenticationProperties properties = GetPropertiesFromState(authorizationResponse.State); if (properties == null) { _logger.WriteWarning("The state field is missing or invalid."); return(null); } // devs will need to hook AuthenticationFailedNotification to avoid having 'raw' runtime errors displayed to users. if (!string.IsNullOrWhiteSpace(authorizationResponse.Error)) { throw CreateOpenIdConnectProtocolException(authorizationResponse); } if (_configuration == null) { _configuration = await Options.ConfigurationManager.GetConfigurationAsync(Context.Request.CallCancelled); } PopulateSessionProperties(authorizationResponse, properties); ClaimsPrincipal user = null; AuthenticationTicket ticket = null; JwtSecurityToken jwt = null; string nonce = null; // Copy and augment to avoid cross request race conditions for updated configurations. var validationParameters = Options.TokenValidationParameters.Clone(); // Hybrid or Implicit flow if (!string.IsNullOrEmpty(authorizationResponse.IdToken)) { var securityTokenReceivedNotification = new SecurityTokenReceivedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { ProtocolMessage = authorizationResponse, }; await Options.Notifications.SecurityTokenReceived(securityTokenReceivedNotification); if (securityTokenReceivedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (securityTokenReceivedNotification.Skipped) { return(null); } user = ValidateToken(authorizationResponse.IdToken, properties, validationParameters, out jwt); if (Options.ProtocolValidator.RequireNonce) { if (string.IsNullOrWhiteSpace(authorizationResponse.Nonce)) { authorizationResponse.Nonce = jwt.Payload.Nonce; } // deletes the nonce cookie nonce = RetrieveNonce(authorizationResponse); } ClaimsIdentity claimsIdentity = user.Identity as ClaimsIdentity; ticket = new AuthenticationTicket(claimsIdentity, properties); var securityTokenValidatedNotification = new SecurityTokenValidatedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { AuthenticationTicket = ticket, ProtocolMessage = authorizationResponse, }; await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); if (securityTokenValidatedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (securityTokenValidatedNotification.Skipped) { return(null); } // Flow possible changes ticket = securityTokenValidatedNotification.AuthenticationTicket; } Options.ProtocolValidator.ValidateAuthenticationResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, ProtocolMessage = authorizationResponse, ValidatedIdToken = jwt, Nonce = nonce }); OpenIdConnectMessage tokenEndpointResponse = null; // Authorization Code or Hybrid flow if (!string.IsNullOrEmpty(authorizationResponse.Code)) { var tokenEndpointRequest = new OpenIdConnectMessage() { ClientId = Options.ClientId, ClientSecret = Options.ClientSecret, Code = authorizationResponse.Code, GrantType = OpenIdConnectGrantTypes.AuthorizationCode, RedirectUri = properties.Dictionary.ContainsKey(OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey) ? properties.Dictionary[OpenIdConnectAuthenticationDefaults.RedirectUriUsedForCodeKey] : string.Empty, }; var authorizationCodeReceivedNotification = new AuthorizationCodeReceivedNotification(Context, Options) { AuthenticationTicket = ticket, Code = authorizationResponse.Code, JwtSecurityToken = jwt, ProtocolMessage = authorizationResponse, RedirectUri = tokenEndpointRequest.RedirectUri, TokenEndpointRequest = tokenEndpointRequest }; // PKCE https://tools.ietf.org/html/rfc7636#section-4.5 string codeVerifier; if (properties.Dictionary.TryGetValue(OAuthConstants.CodeVerifierKey, out codeVerifier)) { tokenEndpointRequest.Parameters.Add(OAuthConstants.CodeVerifierKey, codeVerifier); properties.Dictionary.Remove(OAuthConstants.CodeVerifierKey); } await Options.Notifications.AuthorizationCodeReceived(authorizationCodeReceivedNotification); if (authorizationCodeReceivedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (authorizationCodeReceivedNotification.Skipped) { return(null); } // Flow possible changes authorizationResponse = authorizationCodeReceivedNotification.ProtocolMessage; ticket = authorizationCodeReceivedNotification.AuthenticationTicket; tokenEndpointRequest = authorizationCodeReceivedNotification.TokenEndpointRequest; tokenEndpointResponse = authorizationCodeReceivedNotification.TokenEndpointResponse; jwt = authorizationCodeReceivedNotification.JwtSecurityToken; if (!authorizationCodeReceivedNotification.HandledCodeRedemption && Options.RedeemCode) { tokenEndpointResponse = await RedeemAuthorizationCodeAsync(tokenEndpointRequest); } if (tokenEndpointResponse != null) { var tokenResponseReceivedNotification = new TokenResponseReceivedNotification(Context, Options) { ProtocolMessage = authorizationResponse, TokenEndpointResponse = tokenEndpointResponse }; await Options.Notifications.TokenResponseReceived(tokenResponseReceivedNotification); if (tokenResponseReceivedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (tokenResponseReceivedNotification.Skipped) { return(null); } // no need to validate signature when token is received using "code flow" as per spec // [http://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation]. validationParameters.RequireSignedTokens = false; // At least a cursory validation is required on the new IdToken, even if we've already validated the one from the authorization response. // And we'll want to validate the new JWT in ValidateTokenResponse. JwtSecurityToken tokenEndpointJwt = null; var tokenEndpointUser = ValidateToken(tokenEndpointResponse.IdToken, properties, validationParameters, out tokenEndpointJwt); // Avoid running the event, etc, if it was already done as part of the authorization response validation. if (user == null) { if (Options.ProtocolValidator.RequireNonce) { if (string.IsNullOrWhiteSpace(tokenEndpointResponse.Nonce)) { tokenEndpointResponse.Nonce = tokenEndpointJwt.Payload.Nonce; } // deletes the nonce cookie if (nonce == null) { nonce = RetrieveNonce(tokenEndpointResponse); } } ClaimsIdentity claimsIdentity = tokenEndpointUser.Identity as ClaimsIdentity; ticket = new AuthenticationTicket(claimsIdentity, properties); var securityTokenValidatedNotification = new SecurityTokenValidatedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { AuthenticationTicket = ticket, ProtocolMessage = tokenEndpointResponse, }; await Options.Notifications.SecurityTokenValidated(securityTokenValidatedNotification); if (securityTokenValidatedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (securityTokenValidatedNotification.Skipped) { return(null); } // Flow possible changes ticket = securityTokenValidatedNotification.AuthenticationTicket; } else { if (!string.Equals(jwt.Subject, tokenEndpointJwt.Subject, StringComparison.Ordinal)) { throw new SecurityTokenException("The sub claim does not match in the id_token's from the authorization and token endpoints."); } } jwt = tokenEndpointJwt; } // Validate the token response if it wasn't provided manually if (!authorizationCodeReceivedNotification.HandledCodeRedemption && Options.RedeemCode) { Options.ProtocolValidator.ValidateTokenResponse(new OpenIdConnectProtocolValidationContext() { ClientId = Options.ClientId, ProtocolMessage = tokenEndpointResponse, ValidatedIdToken = jwt, Nonce = nonce }); } } if (Options.SaveTokens && ticket != null) { SaveTokens(ticket.Properties, tokenEndpointResponse ?? authorizationResponse); } return(ticket); } catch (Exception exception) { // We can't await inside a catch block, capture and handle outside. authFailedEx = ExceptionDispatchInfo.Capture(exception); } if (authFailedEx != null) { _logger.WriteError("Exception occurred while processing message: ", authFailedEx.SourceException); // Refresh the configuration for exceptions that may be caused by key rollovers. The user can also request a refresh in the notification. if (Options.RefreshOnIssuerKeyNotFound && authFailedEx.SourceException.GetType().Equals(typeof(SecurityTokenSignatureKeyNotFoundException))) { Options.ConfigurationManager.RequestRefresh(); } var authenticationFailedNotification = new AuthenticationFailedNotification <OpenIdConnectMessage, OpenIdConnectAuthenticationOptions>(Context, Options) { ProtocolMessage = authorizationResponse, Exception = authFailedEx.SourceException }; await Options.Notifications.AuthenticationFailed(authenticationFailedNotification); if (authenticationFailedNotification.HandledResponse) { return(GetHandledResponseTicket()); } if (authenticationFailedNotification.Skipped) { return(null); } authFailedEx.Throw(); } return(null); }