Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

...

Upon receiving a message via webhook, it is essential to return a 202 Accepted response promptly (within 15 seconds) after verifying the message’s legitimacy (optional but recommended). If this acknowledgment is not received in time, the system will retry sending the message according to the retry_intervals configuration, if retries are not acknowledged promptly

Legitimacy Check

While optional, OneStock strongly recommends performing a legitimacy check to ensure that the webhook message is genuinely sent by the OneStock system.

...

  • timestamp: The current timestamp.

  • h[0]: The signature encrypted using the latest hash key.

  • h[1]: The signature encrypted using the previous hash key.

  • h[2]: The signature encrypted using the oldest hash key.

Examples of signature and verification code

Expand
titleExample of signature model

Current timestamp = 1704092400

Encryption of h0 for 1704092400 = 1234 (encryption of the timestamp with the new key)

Encryption of h1 for 1704092400= 9876 (encryption of the timestamp with the previous key)

The signature will be the following : t=1704092400.h0=1234.h1=9876

Resulting the header : Onestock-Signature=t=1704092400.h0=1234,h1=9876

...

Expand
titleExample of signature verification in Ruby
Code Block
languageruby
require 'openssl'
require 'time'

# Method to attempt verification of a webhook signature with multiple secret keys.
# @param request [Object] The incoming request object, containing headers and a body.
# @param previous_keys [Array<String>] An array of secret keys to attempt for signature verification.
# @return [Boolean] True if the signature is verified successfully with any of the keys, otherwise false.
def verify_signature_with_previous_keys(request, previous_keys)
  # Iterates through each provided secret key to attempt signature verification.
  previous_keys.each do |key|
    # If the signature verification is successful with the current key, return true immediately.
    if check_webhook_signature(request, key) {
      return true
    }
  end
  # If none of the keys result in a successful verification, return false.
  return false
end

# Defines a method to check the validity of the webhook signature against the provided secret key.
# @param request [Object] The incoming request object, which contains headers and a body.
# @param secret_key [String] The secret key used for generating and comparing the signature.
# @return [Boolean] True if the signature is valid, otherwise false.
def check_webhook_signature(request, secret_key)
  # Extracts the signature from the request's headers. Expected format: “t=timestamp.h0=h[0].h1=h[1].h2=h[2].h3=h[3]"
  signature_header = request.headers['HTTP_ONESTOCK_SIGNATURE']
  # Reads the request body as a string.
  body = request.body.read
  # Splits the signature header into its constituent parts: the timestamp and the hash values.
  signature_parts = signature_header.split(',')
  # Ensures that there are at least two parts in the signature (the timestamp and at least one hash).
  if signature_parts.size < 2
    return false
  end

  # Extracts and converts the timestamp from the first part of the signature, removing 't=' prefix.
  timestamp = signature_parts[0].sub('t=', '').to_i
  # Verifies that the request is not too old (more than 6 hours in this case) to avoid replay attacks.
  if Time.now.to_i - timestamp > 60 * 60 * 6
    return false
  end

  # Constructs the payload by concatenating the timestamp and the body, separated by a period.
  # This payload is used as the basis for signature generation and comparison.
  payload = "#{timestamp.to_s}.#{body}" # Format: "timestamp.body"
  # Generates the expected signature by hashing the payload with the secret key using SHA-256.
  expected_signature = OpenSSL::HMAC.hexdigest('SHA256', secret_key, payload)
  
  # Iterates over each hash part in the signature, skipping the first part (the timestamp).
  signature_parts.drop(1).each_with_index do |part, index|
    # Removes the hash prefix ('hX=') to isolate the hash value for comparison.
    h = part.sub("h#{index}=", '')
    # If any of the hash values matches the expected signature, the signature is deemed valid.
    if h == expected_signature
      return true
    end  
  end

# Example usage of the new method:
request = # Assume this is the incoming webhook request object.
previous_keys = ['old_secret_key1', 'old_secret_key2', 'current_secret_key']
if verify_signature_with_previous_keys(request, previous_keys)
  puts "Webhook signature verified successfully."
else
  puts "Failed to verify webhook signature."
end

Webhook Acknowledgment Best Practices

To avoid blocking the entire message queue when processing webhooks, it is crucial to focus on the signature validation first and handle any additional checks asynchronously.

...