Module: WorkOS::Webhooks

Defined in:
lib/workos/webhooks.rb

Overview

The Webhooks module provides convenience methods for working with the WorkOS webhooks. You’ll need to extract the signature header and payload from the webhook request sig_header = request.headers payload = request.body.read

The secret is the Webhook Secret from your WorkOS Dashboard The tolerance is for the timestamp validation

Constant Summary collapse

DEFAULT_TOLERANCE =
180

Class Method Summary collapse

Class Method Details

.compute_signature(timestamp:, payload:, secret:) ⇒ Object

Computes expected signature rubocop:disable Layout/LineLength

rubocop:enable Layout/LineLength

Examples:

WorkOS::Webhooks.compute_signature(
  timestamp: '1626125972272',
  payload: "{"id": "wh_123","data":{"id":"directory_user_01FAEAJCR3ZBZ30D8BD1924TVG","state":"active","emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"idp_id":"00u1e8mutl6wlH3lL4x7","object":"directory_user","username":"blair@foo-corp.com","last_name":"Lunchford","first_name":"Blair","directory_id":"directory_01F9M7F68PZP8QXP8G7X5QRHS7","raw_attributes":{"name":{"givenName":"Blair","familyName":"Lunchford","middleName":"Elizabeth","honorificPrefix":"Ms."},"title":"Developer Success Engineer","active":true,"emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"groups":[],"locale":"en-US","schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"userName":"blair@foo-corp.com","addresses":[{"region":"CA","primary":true,"locality":"San Francisco","postalCode":"94016"}],"externalId":"00u1e8mutl6wlH3lL4x7","displayName":"Blair Lunchford","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"manager":{"value":"2","displayName":"Kate Chapman"},"division":"Engineering","department":"Customer Success"}}},"event":"dsync.user.created"}",
  secret: 'LJlTiC19GmCKWs8AE0IaOQcos',
)

=> '80f7ab7efadc306eb5797c588cee9410da9be4416782b497bf1e1bf4175fb928'

Parameters:

  • timestamp (String)

    The timestamp from the webhook signature.

  • payload (String)

    The payload from the webhook sent by WorkOS. This is the RAW_POST_DATA of the request.

  • secret (String)

    The webhook secret from the WorkOS dashboard.

Returns:

  • String



153
154
155
156
157
158
159
160
161
# File 'lib/workos/webhooks.rb', line 153

def compute_signature(
  timestamp:,
  payload:,
  secret:
)
  unhashed_string = "#{timestamp}.#{payload}"
  digest = OpenSSL::Digest.new('sha256')
  OpenSSL::HMAC.hexdigest(digest, secret, unhashed_string)
end

.construct_event(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ WorkOS::Webhook

Initializes an Event object from a JSON payload rubocop:disable Layout/LineLength

rubocop:enable Layout/LineLength

Examples:

WorkOS::Webhooks.construct_event(
  payload: "{"id": "wh_123","data":{"id":"directory_user_01FAEAJCR3ZBZ30D8BD1924TVG","state":"active","emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"idp_id":"00u1e8mutl6wlH3lL4x7","object":"directory_user","username":"blair@foo-corp.com","last_name":"Lunceford","first_name":"Blair","directory_id":"directory_01F9M7F68PZP8QXP8G7X5QRHS7","raw_attributes":{"name":{"givenName":"Blair","familyName":"Lunceford","middleName":"Elizabeth","honorificPrefix":"Ms."},"title":"Developer Success Engineer","active":true,"emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"groups":[],"locale":"en-US","schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"userName":"blair@foo-corp.com","addresses":[{"region":"CO","primary":true,"locality":"Steamboat Springs","postalCode":"80487"}],"externalId":"00u1e8mutl6wlH3lL4x7","displayName":"Blair Lunceford","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"manager":{"value":"2","displayName":"Kathleen Chung"},"division":"Engineering","department":"Customer Success"}}},"event":"dsync.user.created"}",
  sig_header: 't=1626125972272, v1=80f7ab7efadc306eb5797c588cee9410da9be4416782b497bf1e1bf4175fb928',
  secret: 'LJlTiC19GmCKWs8AE0IaOQcos',
)

=> #<WorkOS::Webhook:0x00007fa64b980910 @event="dsync.user.created", @data={:id=>"directory_user_01FAEAJCR3ZBZ30D8BD1924TVG", :state=>"active", :emails=>[{:type=>"work", :value=>"[email protected]", :primary=>true}], :idp_id=>"00u1e8mutl6wlH3lL4x7", :object=>"directory_user", :username=>"[email protected]", :last_name=>"Lunceford", :first_name=>"Blair", :directory_id=>"directory_01F9M7F68PZP8QXP8G7X5QRHS7", :raw_attributes=>{:name=>{:givenName=>"Blair", :familyName=>"Lunceford", :middleName=>"Elizabeth", :honorificPrefix=>"Ms."}, :title=>"Developer Success Engineer", :active=>true, :emails=>[{:type=>"work", :value=>"[email protected]", :primary=>true}], :groups=>[], :locale=>"en-US", :schemas=>["urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"], :userName=>"[email protected]", :addresses=>[{:region=>"CO", :primary=>true, :locality=>"Steamboat Springs", :postalCode=>"80487"}], :externalId=>"00u1e8mutl6wlH3lL4x7", :displayName=>"Blair Lunceford", :"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"=>{:manager=>{:value=>"2", :displayName=>"Kathleen Chung"}, :division=>"Engineering", :department=>"Customer Success"}}}>

Parameters:

  • payload (String)

    The payload from the webhook sent by WorkOS. This is the RAW_POST_DATA of the request.

  • sig_header (String)

    The signature from the webhook sent by WorkOS.

  • secret (String)

    The webhook secret from the WorkOS dashboard.

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE)

    The time tolerance in seconds for the webhook.

Returns:



37
38
39
40
41
42
43
44
45
# File 'lib/workos/webhooks.rb', line 37

def construct_event(
  payload:,
  sig_header:,
  secret:,
  tolerance: DEFAULT_TOLERANCE
)
  verify_header(payload: payload, sig_header: sig_header, secret: secret, tolerance: tolerance)
  WorkOS::Webhook.new(payload)
end

.get_timestamp_and_signature_hash(sig_header:) ⇒ Object

Extracts timestamp and signature hash from WorkOS-Signature header

Examples:

WorkOS::Webhooks.get_timestamp_and_signature_hash(
  sig_header: 't=1626125972272, v1=80f7ab7efadc306eb5797c588cee9410da9be4416782b497bf1e1bf4175fb928',
)

=> ['1626125972272', '80f7ab7efadc306eb5797c588cee9410da9be4416782b497bf1e1bf4175fb928']

Parameters:

  • sig_header (String)

    The signature from the webhook sent by WorkOS.

Returns:

  • Array



118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# File 'lib/workos/webhooks.rb', line 118

def get_timestamp_and_signature_hash(
  sig_header:
)
  timestamp, signature_hash = sig_header.split(', ')

  if timestamp.nil? || signature_hash.nil?
    raise WorkOS::SignatureVerificationError.new(
      message: 'Unable to extract timestamp and signature hash from header',
    )
  end

  timestamp = timestamp.sub('t=', '')
  signature_hash = signature_hash.sub('v1=', '')

  [timestamp, signature_hash]
end

.secure_compare(str_a:, str_b:) ⇒ Object

Constant time string comparison to prevent timing attacks Code borrowed from ActiveSupport



165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/workos/webhooks.rb', line 165

def secure_compare(
  str_a:,
  str_b:
)
  return false unless str_a.bytesize == str_b.bytesize

  l = str_a.unpack("C#{str_a.bytesize}")

  res = 0
  str_b.each_byte { |byte| res |= byte ^ l.shift }

  res.zero?
end

.verify_header(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE) ⇒ Object

Verifies WorkOS-Signature header from request rubocop:disable Layout/LineLength

rubocop:enable Layout/LineLength rubocop:disable Metrics/AbcSize

Examples:

WorkOS::Webhooks.verify_header(
  payload: "{"id": "wh_123","data":{"id":"directory_user_01FAEAJCR3ZBZ30D8BD1924TVG","state":"active","emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"idp_id":"00u1e8mutl6wlH3lL4x7","object":"directory_user","username":"blair@foo-corp.com","last_name":"Lunchford","first_name":"Blair","directory_id":"directory_01F9M7F68PZP8QXP8G7X5QRHS7","raw_attributes":{"name":{"givenName":"Blair","familyName":"Lunchford","middleName":"Elizabeth","honorificPrefix":"Ms."},"title":"Developer Success Engineer","active":true,"emails":[{"type":"work","value":"blair@foo-corp.com","primary":true}],"groups":[],"locale":"en-US","schemas":["urn:ietf:params:scim:schemas:core:2.0:User","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"],"userName":"blair@foo-corp.com","addresses":[{"region":"CA","primary":true,"locality":"San Francisco","postalCode":"94016"}],"externalId":"00u1e8mutl6wlH3lL4x7","displayName":"Blair Lunchford","urn:ietf:params:scim:schemas:extension:enterprise:2.0:User":{"manager":{"value":"2","displayName":"Kate Chapman"},"division":"Engineering","department":"Customer Success"}}},"event":"dsync.user.created"}",
  sig_header: 't=1626125972272, v1=80f7ab7efadc306eb5797c588cee9410da9be4416782b497bf1e1bf4175fb928',
  secret: 'LJlTiC19GmCKWs8AE0IaOQcos',
)

=> true

Parameters:

  • payload (String)

    The payload from the webhook sent by WorkOS. This is the RAW_POST_DATA of the request.

  • sig_header (String)

    The signature from the webhook sent by WorkOS.

  • secret (String)

    The webhook secret from the WorkOS dashboard.

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE)

    The time tolerance in seconds for the webhook.

Returns:

  • Boolean



67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/workos/webhooks.rb', line 67

def verify_header(
  payload:,
  sig_header:,
  secret:,
  tolerance: DEFAULT_TOLERANCE
)
  begin
    timestamp, signature_hash = get_timestamp_and_signature_hash(sig_header: sig_header)
  rescue StandardError
    raise WorkOS::SignatureVerificationError.new(
      message: 'Unable to extract timestamp and signature hash from header',
    )
  end

  if signature_hash.empty?
    raise WorkOS::SignatureVerificationError.new(
      message: 'No signature hash found with expected scheme v1',
    )
  end

  timestamp_to_time = Time.at(timestamp.to_i / 1000)

  if timestamp_to_time < Time.now - tolerance
    raise WorkOS::SignatureVerificationError.new(
      message: 'Timestamp outside the tolerance zone',
    )
  end

  expected_sig = compute_signature(timestamp: timestamp, payload: payload, secret: secret)
  unless secure_compare(str_a: expected_sig, str_b: signature_hash)
    raise WorkOS::SignatureVerificationError.new(
      message: 'Signature hash does not match the expected signature hash for payload',
    )
  end

  true
end