Class: Paymos::WebhookVerifier

Inherits:
Object
  • Object
show all
Defined in:
lib/paymos/webhook_verifier.rb,
sig/paymos.rbs

Instance Method Summary collapse

Constructor Details

#initialize(secret, tolerance: 300, clock: -> { Time.now.to_i }) ⇒ WebhookVerifier

Returns a new instance of WebhookVerifier.

Parameters:

  • secret (String)
  • tolerance: (Integer) (defaults to: 300)
  • clock: (Object) (defaults to: -> { Time.now.to_i })

Raises:

  • (ArgumentError)


9
10
11
12
13
14
15
16
17
18
19
# File 'lib/paymos/webhook_verifier.rb', line 9

def initialize(secret, tolerance: 300, clock: -> { Time.now.to_i })
  unless secret.is_a?(String) && !secret.strip.empty?
    raise ArgumentError,
          'Webhook secret must be a non-empty string'
  end
  raise ArgumentError, 'Webhook tolerance must be non-negative' unless tolerance.is_a?(Integer) && tolerance >= 0

  @secret = secret
  @tolerance = tolerance
  @clock = clock
end

Instance Method Details

#assert_valid(signature_header, raw_body, now: nil) ⇒ void

This method returns an undefined value.

Parameters:

  • signature_header (String)
  • raw_body (String)
  • now: (Integer, nil) (defaults to: nil)

Raises:

  • (ArgumentError)


28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# File 'lib/paymos/webhook_verifier.rb', line 28

def assert_valid(signature_header, raw_body, now: nil)
  raise ArgumentError, 'Raw webhook body must be a string' unless raw_body.is_a?(String)

  timestamp, signatures = parse_header(signature_header)
  current = now.nil? ? @clock.call.to_i : now.to_i
  if (current - timestamp).abs > @tolerance
    raise TimestampSkewError,
          'Webhook timestamp is outside the allowed tolerance'
  end

  expected = OpenSSL::HMAC.hexdigest('SHA256', @secret, "#{timestamp}.#{raw_body}")
  valid = signatures.any? { |value| secure_compare(expected, value.downcase) }
  raise SignatureMismatchError, 'Webhook signature does not match payload' unless valid

  nil
end

#construct_event(signature_header, raw_body, now: nil) ⇒ WebhookEvent[Hash[String, untyped]]

Parameters:

  • signature_header (String)
  • raw_body (String)
  • now: (Integer, nil) (defaults to: nil)

Returns:



45
46
47
48
49
50
# File 'lib/paymos/webhook_verifier.rb', line 45

def construct_event(signature_header, raw_body, now: nil)
  assert_valid(signature_header, raw_body, now: now)
  WebhookEvent.from(JSON.parse(raw_body))
rescue JSON::ParserError => e
  raise Error, "Webhook payload is invalid JSON: #{e.message}"
end

#verify(signature_header, raw_body, now: nil) ⇒ Boolean

Parameters:

  • signature_header (String)
  • raw_body (String)
  • now: (Integer, nil) (defaults to: nil)

Returns:

  • (Boolean)


21
22
23
24
25
26
# File 'lib/paymos/webhook_verifier.rb', line 21

def verify(signature_header, raw_body, now: nil)
  assert_valid(signature_header, raw_body, now: now)
  true
rescue SignatureMismatchError, TimestampSkewError
  false
end