protected virtual IActionResult EnsureSecureConnection(string receiverName, HttpRequest request) { if (request == null) { throw new ArgumentNullException(nameof(request)); } // Check to see if we have been configured to ignore this check if (ReceiverConfig.IsTrue(WebHookConstants.DisableHttpsCheckConfigurationKey)) { return(null); } // Require HTTP unless request is local if (!request.IsLocal() && !request.IsHttps) { Logger.LogError( 500, "The '{ReceiverName}' WebHook receiver requires HTTPS in order to be secure. " + "Please register a WebHook URI of type '{SchemeName}'.", receiverName, Uri.UriSchemeHttps); var message = string.Format( CultureInfo.CurrentCulture, Resources.Security_NoHttps, receiverName, Uri.UriSchemeHttps); var noHttps = WebHookResultUtilities.CreateErrorResult(message); return(noHttps); } return(null); }
/// <summary> /// Decode the given <paramref name="hexEncodedValue"/>. /// </summary> /// <param name="hexEncodedValue">The hex-encoded <see cref="string"/>.</param> /// <param name="signatureHeaderName"> /// The name of the HTTP header containing the <paramref name="hexEncodedValue"/>. /// </param> /// <param name="errorResult"> /// Set to <see langword="null"/> if decoding is successful. Otherwise, an <see cref="IActionResult"/> that /// when executed will produce a response containing details about the problem. /// </param> /// <returns> /// If successful, the <see cref="byte"/> array containing the decoded hash. <see langword="null"/> if any /// issues occur. /// </returns> protected virtual byte[] GetDecodedHash( string hexEncodedValue, string signatureHeaderName, out IActionResult errorResult) { try { var decodedHash = EncodingUtilities.FromHex(hexEncodedValue); errorResult = null; return(decodedHash); } catch (Exception ex) { Logger.LogError( 401, ex, "The '{HeaderName}' header value is invalid. It must be a valid hex-encoded string.", signatureHeaderName); } var message = string.Format( CultureInfo.CurrentCulture, Resources.Security_BadHeaderEncoding, signatureHeaderName); errorResult = WebHookResultUtilities.CreateErrorResult(message); return(null); }
private IActionResult CreateUnsupportedMediaTypeResult(string message) { _logger.LogInformation(0, message); // ??? Should we instead provide CreateErrorResult(...) overloads with `int statusCode` parameters? var badMethod = WebHookResultUtilities.CreateErrorResult(message); badMethod.StatusCode = StatusCodes.Status415UnsupportedMediaType; return(badMethod); }
protected virtual IActionResult CreateBadSignatureResult(string receiverName, string signatureHeaderName) { Logger.LogError( 402, "The WebHook signature provided by the '{HeaderName}' header field does not match the value " + "expected by the '{ReceiverName}' receiver. WebHook request is invalid.", signatureHeaderName, receiverName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifySignature_BadSignature, signatureHeaderName, receiverName); var badSignature = WebHookResultUtilities.CreateErrorResult(message); return(badSignature); }
private async Task <IActionResult> GetChallengeResponse( WebHookGetRequest getMetadata, string receiverName, HttpRequest request, RouteData routeData) { // 1. Verify that we have the secret as an app setting. var secretKey = await GetReceiverConfig( request, routeData, receiverName, getMetadata.SecretKeyMinLength, getMetadata.SecretKeyMaxLength); if (secretKey == null) { return(new NotFoundResult()); } // 2. Get the 'challenge' parameter from the request URI. var challenge = request.Query[getMetadata.ChallengeQueryParameterName]; if (StringValues.IsNullOrEmpty(challenge)) { Logger.LogError( 400, "The WebHook verification request must contain a '{ParameterName}' query parameter.", getMetadata.ChallengeQueryParameterName); var message = string.Format( CultureInfo.CurrentCulture, Resources.General_MissingQueryParameter, getMetadata.ChallengeQueryParameterName); var noChallenge = WebHookResultUtilities.CreateErrorResult(message); return(noChallenge); } // 3. Echo the challenge back to the caller. return(new ContentResult { Content = challenge, }); }
/// <inheritdoc /> public void OnResourceExecuting(ResourceExecutingContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (!context.RouteData.TryGetReceiverName(out var receiverName)) { // Not a WebHook request. return; } var bindingMetadata = _bindingMetadata.FirstOrDefault(metadata => metadata.IsApplicable(receiverName)); if (bindingMetadata == null) { // Receiver has no additional parameters. return; } var request = context.HttpContext.Request; var headers = request.Headers; var query = request.Query; for (var i = 0; i < bindingMetadata.Parameters.Count; i++) { var parameter = bindingMetadata.Parameters[i]; if (parameter.IsRequired) { var sourceName = parameter.SourceName; var found = parameter.IsQueryParameter ? VerifyQueryParameter(query, sourceName, receiverName, out var message) : VerifyHeader(headers, sourceName, receiverName, out message); if (!found) { // Do not return after first error. Instead log about all issues. context.Result = WebHookResultUtilities.CreateErrorResult(message); } } } }
private IActionResult CreateBadMethodResult(string methodName, string receiverName) { _logger.LogError( 0, "The HTTP '{RequestMethod}' method is not supported by the '{ReceiverName}' WebHook receiver.", methodName, receiverName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyMethod_BadMethod, methodName, receiverName); // ??? Should we instead provide CreateErrorResult(...) overloads with `int statusCode` parameters? var badMethod = WebHookResultUtilities.CreateErrorResult(message); badMethod.StatusCode = StatusCodes.Status405MethodNotAllowed; return(badMethod); }
/// <inheritdoc /> public void OnException(ExceptionContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } // Apply to all requests matching an action with the WebHook route template. if (context.RouteData.TryGetWebHookReceiverName(out var receiverName)) { _logger.LogError( 0, context.Exception, "WebHook receiver '{ReceiverName}' could not process WebHook due to error.", receiverName); var result = WebHookResultUtilities.CreateErrorResult( context.Exception, includeErrorDetail: _hostingEnvironment.IsDevelopment()); context.Result = result; } }
protected virtual string GetRequestHeader( HttpRequest request, string headerName, out IActionResult errorResult) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (headerName == null) { throw new ArgumentNullException(nameof(headerName)); } if (!request.Headers.TryGetValue(headerName, out var headers) || headers.Count != 1) { var headersCount = headers.Count; Logger.LogInformation( 400, "Expecting exactly one '{HeaderName}' header field in the WebHook request but found " + "{HeaderCount}. Please ensure the request contains exactly one '{HeaderName}' header field.", headerName, headersCount); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifySignature_BadHeader, headerName, headersCount); errorResult = WebHookResultUtilities.CreateErrorResult(message); return(null); } errorResult = null; return(headers); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var routeData = context.RouteData; if (!routeData.TryGetReceiverName(out var receiverName) || !IsApplicable(receiverName)) { await next(); return; } // 1. Confirm we were reached using HTTPS. var request = context.HttpContext.Request; var errorResult = EnsureSecureConnection(receiverName, request); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get XElement and SalesforceNotifications from the request body. var data = await ReadAsXmlAsync(context); if (data == null) { var modelState = context.ModelState; if (modelState.IsValid) { // ReadAsXmlAsync returns null when model state is valid only when other filters will log and // return errors about the same conditions. Let those filters run. await next(); } else { context.Result = WebHookResultUtilities.CreateErrorResult(modelState); } return; } // Got a valid XML body. From this point on, all responses should contain XML. var notifications = new SalesforceNotifications(data); // 3. Ensure that the organization ID exists and matches the expected value. var organizationId = GetShortOrganizationId(notifications.OrganizationId); if (string.IsNullOrEmpty(organizationId)) { Logger.LogError( 0, "The HTTP request body did not contain a required '{PropertyName}' property.", nameof(notifications.OrganizationId)); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyOrganization_MissingValue, nameof(notifications.OrganizationId)); context.Result = await _resultCreator.GetFailedResultAsync(message); return; } var secret = await GetReceiverConfig( request, routeData, SalesforceConstants.ConfigurationName, SalesforceConstants.SecretKeyMinLength, SalesforceConstants.SecretKeyMaxLength); var secretKey = GetShortOrganizationId(secret); if (!SecretEqual(organizationId, secretKey)) { Logger.LogError( 1, "The '{PropertyName}' value provided in the HTTP request body did not match the expected value.", nameof(notifications.OrganizationId)); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyOrganization_BadValue, nameof(notifications.OrganizationId)); context.Result = await _resultCreator.GetFailedResultAsync(message); return; } // 4. Get the event name. var eventName = notifications.ActionId; if (string.IsNullOrEmpty(eventName)) { Logger.LogError( 2, "The HTTP request body did not contain a required '{PropertyName}' property.", nameof(notifications.ActionId)); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyOrganization_MissingValue, nameof(notifications.ActionId)); context.Result = await _resultCreator.GetFailedResultAsync(message); return; } // 5. Success. Provide request data and event name for model binding. routeData.Values[WebHookConstants.EventKeyName] = eventName; context.HttpContext.Items[typeof(XElement)] = data; context.HttpContext.Items[typeof(SalesforceNotifications)] = notifications; await next(); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } // 1. Confirm this filter applies. var routeData = context.RouteData; if (!routeData.TryGetReceiverName(out var receiverName) || !IsApplicable(receiverName)) { await next(); return; } // 2. Get JObject from the request body. var data = await ReadAsJsonAsync(context); if (data == null) { var modelState = context.ModelState; if (modelState.IsValid) { // ReadAsJsonAsync returns null when model state is valid only when other filters will log and // return errors about the same conditions. Let those filters run. await next(); } else { context.Result = WebHookResultUtilities.CreateErrorResult(modelState); } return; } // 3. Ensure the notification identifier exists. var notificationId = data.Value <string>(StripeConstants.NotificationIdPropertyName); if (string.IsNullOrEmpty(notificationId)) { Logger.LogError( 0, "The HTTP request body did not contain a required '{PropertyName}' property.", StripeConstants.NotificationIdPropertyName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyNotification_MissingValue, StripeConstants.NotificationIdPropertyName); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } // 4. Ensure the event name exists. // ??? Should this be optional? It's optional in `StripeWebHookReceiver` but Stripe docs imply otherwise. // ?? Should this be done later -- as in `StripeWebHookReceiver`? Seems useful even for test events. var eventName = data.Value <string>(StripeConstants.EventPropertyName); if (string.IsNullOrEmpty(eventName)) { Logger.LogError( 1, "The HTTP request body did not contain a required '{PropertyName}' property.", StripeConstants.EventPropertyName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyNotification_MissingValue, StripeConstants.EventPropertyName); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } // 5. Handle test events or get confirmed data. // `WebHookVerifyCodeFilter` has already handled the _useDirectWebHook verification. if (IsTestEvent(notificationId)) { // Will short-circuit test events if !_passThroughTestEvents later, in StripeTestEventResponseFilter. if (_passThroughTestEvents) { Logger.LogInformation(2, "Received a Stripe Test Event."); } } else if (!_useDirectWebHook) { // Callback to get the real data. data = await GetEventDataAsync(context.HttpContext.Request, routeData, notificationId); if (data == null) { var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyNotification_BadId, notificationId); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } } // 6. Success. Provide request data and event name for model binding. routeData.Values[WebHookConstants.EventKeyName] = eventName; context.HttpContext.Items[typeof(JObject)] = data; context.HttpContext.Items[typeof(StripeEvent)] = data.ToObject <StripeEvent>(); await next(); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var routeData = context.RouteData; if (!routeData.TryGetWebHookReceiverName(out var receiverName) || !IsApplicable(receiverName)) { await next(); return; } // 1. Confirm we were reached using HTTPS. var request = context.HttpContext.Request; var errorResult = EnsureSecureConnection(receiverName, request); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get IFormCollection from the request body. var data = await ReadAsFormDataAsync(context); if (data == null) { // ReadAsFormDataAsync returns null only when other filters will log and return errors about the same // conditions. Let those filters run. await next(); return; } // 3. Ensure that the token exists and matches the expected value. string token = data[SlackConstants.TokenRequestFieldName]; if (string.IsNullOrEmpty(token)) { Logger.LogError( 0, "The HTTP request body did not contain a required '{PropertyName}' property.", SlackConstants.TokenRequestFieldName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyToken_MissingValue, SlackConstants.TokenRequestFieldName); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } var secretKey = GetSecretKey( ReceiverName, routeData, SlackConstants.SecretKeyMinLength, SlackConstants.SecretKeyMaxLength); if (!SecretEqual(token, secretKey)) { Logger.LogError( 1, "The '{PropertyName}' value provided in the HTTP request body did not match the expected value.", SlackConstants.TokenRequestFieldName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyToken_BadValue, SlackConstants.TokenRequestFieldName); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } // 4. Get the event name and subtext. string eventName = data[SlackConstants.TriggerRequestFieldName]; if (eventName != null) { // Trigger was supplied. Remove the trigger word to get subtext. string text = data[SlackConstants.TextRequestFieldName]; routeData.Values[SlackConstants.SubtextRequestKeyName] = GetSubtext(eventName, text); } else if ((eventName = data[SlackConstants.CommandRequestFieldName]) != null) { // Command was supplied. No need to set subtext. } else { // Trigger and command were omitted. Set subtext to the full text (if any). eventName = data[SlackConstants.TextRequestFieldName]; routeData.Values[SlackConstants.SubtextRequestKeyName] = eventName; } if (string.IsNullOrEmpty(eventName)) { Logger.LogError( 2, "The HTTP request body did not contain a required '{PropertyName1}', '{PropertyName2}', or " + "'{PropertyName3}' property.", SlackConstants.TriggerRequestFieldName, SlackConstants.CommandRequestFieldName, SlackConstants.TextRequestFieldName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyToken_MissingValues, SlackConstants.TriggerRequestFieldName, SlackConstants.CommandRequestFieldName, SlackConstants.TextRequestFieldName); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } // 5. Success. Provide event name for model binding. routeData.Values[WebHookConstants.EventKeyName] = eventName; await next(); }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var routeData = context.RouteData; var request = context.HttpContext.Request; if (routeData.TryGetReceiverName(out var receiverName) && IsApplicable(receiverName) && HttpMethods.IsPost(request.Method)) { // 1. Get the expected hash from the signature headers. var header = GetRequestHeader(request, PusherConstants.SignatureHeaderName, out var errorResult); if (errorResult != null) { context.Result = errorResult; return; } var expectedHash = GetDecodedHash(header, PusherConstants.SignatureHeaderName, out errorResult); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get the configured secret key. var lookupTable = await GetSecretLookupTable(request, routeData); if (lookupTable == null) { context.Result = new NotFoundResult(); return; } var applicationKey = GetRequestHeader(request, PusherConstants.SignatureKeyHeaderName, out errorResult); if (errorResult != null) { context.Result = errorResult; return; } if (!lookupTable.TryGetValue(applicationKey, out var secretKey)) { Logger.LogError( 0, "The '{HeaderName}' header value of '{HeaderValue}' is not recognized as a valid " + "application key. Please ensure the correct application key / secret key pairs have " + "been configured.", PusherConstants.SignatureKeyHeaderName, applicationKey); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_SecretNotFound, PusherConstants.SignatureKeyHeaderName, applicationKey); context.Result = WebHookResultUtilities.CreateErrorResult(message); return; } var secret = Encoding.UTF8.GetBytes(secretKey); // 3. Get the actual hash of the request body. var actualHash = await GetRequestBodyHash_SHA256(request, secret); // 4. Verify that the actual hash matches the expected hash. if (!SecretEqual(expectedHash, actualHash)) { // Log about the issue and short-circuit remainder of the pipeline. errorResult = CreateBadSignatureResult(receiverName, PusherConstants.SignatureHeaderName); context.Result = errorResult; return; } } await next(); }
/// <inheritdoc /> public void OnResourceExecuting(ResourceExecutingContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } var routeData = context.RouteData; if (!routeData.TryGetWebHookReceiverName(out var receiverName)) { // Not a WebHook request. return; } var bindingMetadata = _bindingMetadata.FirstOrDefault(metadata => metadata.IsApplicable(receiverName)); if (bindingMetadata == null) { // Receiver has no additional parameters. return; } var request = context.HttpContext.Request; for (var i = 0; i < bindingMetadata.Parameters.Count; i++) { var parameter = bindingMetadata.Parameters[i]; if (parameter.IsRequired) { bool found; string message; var sourceName = parameter.SourceName; switch (parameter.ParameterType) { case WebHookParameterType.Header: found = VerifyHeader(request.Headers, sourceName, receiverName, out message); break; case WebHookParameterType.RouteValue: found = VerifyRouteData(routeData, sourceName, receiverName, out message); break; case WebHookParameterType.QueryParameter: found = VerifyQueryParameter(request.Query, sourceName, receiverName, out message); break; default: message = string.Format( CultureInfo.CurrentCulture, Resources.General_InvalidEnumValue, nameof(WebHookParameterType), parameter.ParameterType); throw new InvalidOperationException(message); } if (!found) { // Do not return after first error. Instead log about all issues. context.Result = WebHookResultUtilities.CreateErrorResult(message); } } } }
/// <inheritdoc /> public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next) { if (context == null) { throw new ArgumentNullException(nameof(context)); } if (next == null) { throw new ArgumentNullException(nameof(next)); } var routeData = context.RouteData; var request = context.HttpContext.Request; if (routeData.TryGetReceiverName(out var receiverName) && IsApplicable(receiverName) && HttpMethods.IsPost(request.Method)) { // 1. Get the expected hash from the signature header. var header = GetRequestHeader(request, GitHubConstants.SignatureHeaderName, out var errorResult); if (errorResult != null) { context.Result = errorResult; return; } var values = new TrimmingTokenizer(header, PairSeparators); var enumerator = values.GetEnumerator(); enumerator.MoveNext(); var headerKey = enumerator.Current; if (values.Count != 2 || !StringSegment.Equals( headerKey, GitHubConstants.SignatureHeaderKey, StringComparison.OrdinalIgnoreCase)) { Logger.LogError( 1, "Invalid '{HeaderName}' header value. Expecting a value of '{Key}={Value}'.", GitHubConstants.SignatureHeaderName, GitHubConstants.SignatureHeaderKey, "<value>"); var message = string.Format( CultureInfo.CurrentCulture, Resources.SignatureFilter_BadHeaderValue, GitHubConstants.SignatureHeaderName, GitHubConstants.SignatureHeaderKey, "<value>"); errorResult = WebHookResultUtilities.CreateErrorResult(message); context.Result = errorResult; return; } enumerator.MoveNext(); var headerValue = enumerator.Current.Value; var expectedHash = GetDecodedHash(headerValue, GitHubConstants.SignatureHeaderName, out errorResult); if (errorResult != null) { context.Result = errorResult; return; } // 2. Get the configured secret key. var secretKey = await GetReceiverConfig( request, routeData, ReceiverName, GitHubConstants.SecretKeyMinLength, GitHubConstants.SecretKeyMaxLength); if (secretKey == null) { context.Result = new NotFoundResult(); return; } var secret = Encoding.UTF8.GetBytes(secretKey); // 3. Get the actual hash of the request body. var actualHash = await GetRequestBodyHash_SHA1(request, secret); // 4. Verify that the actual hash matches the expected hash. if (!SecretEqual(expectedHash, actualHash)) { // Log about the issue and short-circuit remainder of the pipeline. errorResult = CreateBadSignatureResult(receiverName, GitHubConstants.SignatureHeaderName); context.Result = errorResult; return; } } await next(); }
protected virtual async Task <IActionResult> EnsureValidCode( HttpRequest request, RouteData routeData, string receiverName) { if (request == null) { throw new ArgumentNullException(nameof(request)); } if (routeData == null) { throw new ArgumentNullException(nameof(routeData)); } if (receiverName == null) { throw new ArgumentNullException(nameof(receiverName)); } var result = EnsureSecureConnection(receiverName, request); if (result != null) { return(result); } var code = request.Query[WebHookConstants.CodeQueryParameterName]; if (StringValues.IsNullOrEmpty(code)) { Logger.LogError( 400, "The WebHook verification request must contain a '{ParameterName}' query parameter.", WebHookConstants.CodeQueryParameterName); var message = string.Format( CultureInfo.CurrentCulture, Resources.General_MissingQueryParameter, WebHookConstants.CodeQueryParameterName); var noCode = WebHookResultUtilities.CreateErrorResult(message); return(noCode); } var secretKey = await GetReceiverConfig( request, routeData, receiverName, WebHookConstants.CodeParameterMinLength, WebHookConstants.CodeParameterMaxLength); if (secretKey == null) { return(new NotFoundResult()); } if (!SecretEqual(code, secretKey)) { Logger.LogError( 401, "The '{ParameterName}' query parameter provided in the HTTP request did not match the " + "expected value.", WebHookConstants.CodeQueryParameterName); var message = string.Format( CultureInfo.CurrentCulture, Resources.VerifyCode_BadCode, WebHookConstants.CodeQueryParameterName); var invalidCode = WebHookResultUtilities.CreateErrorResult(message); return(invalidCode); } return(null); }