Module: WorkOS::Util::Signature

Defined in:
lib/workos/util/signature.rb

Class Method Summary collapse

Class Method Details

.compute(payload:, timestamp:, secret:) ⇒ String

Computes the expected signature hash for a webhook or action payload.

Parameters:

  • payload (String)

    Raw request body.

  • timestamp (String)

    Timestamp extracted from the signature header.

  • secret (String)

    Webhook or action signing secret.

Returns:

  • (String)


17
18
19
# File 'lib/workos/util/signature.rb', line 17

def compute(payload:, timestamp:, secret:)
  OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}")
end

.parse_header(sig_header) ⇒ Array(String, String)

Parses the WorkOS signature header.

Parameters:

  • sig_header (String)

    Header value in ‘t=…, v1=…` format.

Returns:

  • (Array(String, String))

Raises:

  • (ArgumentError)

    If the header is missing or malformed.



26
27
28
29
30
31
32
33
34
35
# File 'lib/workos/util/signature.rb', line 26

def parse_header(sig_header)
  raise ArgumentError, "Signature header missing" if sig_header.nil? || sig_header.empty?

  parts = sig_header.split(",").map(&:strip)
  timestamp = parts.find { |part| part.start_with?("t=") }&.sub(/\At=/, "")
  signature = parts.find { |part| part.start_with?("v1=") }&.sub(/\Av1=/, "")
  raise ArgumentError, "Unable to extract timestamp and signature hash from header" if timestamp.nil? || signature.nil?

  [timestamp, signature]
end

.secure_compare(a, b) ⇒ Boolean

Compares two signature hashes in constant time.

Parameters:

  • a (String)
  • b (String)

Returns:

  • (Boolean)


42
43
44
45
46
47
48
49
50
51
52
# File 'lib/workos/util/signature.rb', line 42

def secure_compare(a, b)
  return false if a.bytesize != b.bytesize

  OpenSSL.fixed_length_secure_compare(a, b)
rescue NoMethodError
  left = a.unpack("C*")
  result = 0
  index = -1
  b.each_byte { |byte| result |= byte ^ left[index += 1] }
  result.zero?
end