Class: WorkOS::Webhooks

Inherits:
Object
  • Object
show all
Defined in:
lib/workos/webhooks.rb

Constant Summary collapse

DEFAULT_TOLERANCE_SECONDS =
180

Instance Method Summary collapse

Constructor Details

#initialize(client) ⇒ Webhooks

Returns a new instance of Webhooks.



9
10
11
# File 'lib/workos/webhooks.rb', line 9

def initialize(client)
  @client = client
end

Instance Method Details

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

Compute the HMAC-SHA256 hex signature for a (timestamp, payload) pair. Exposed publicly so users can build their own verification flow.



215
216
217
# File 'lib/workos/webhooks.rb', line 215

def compute_signature(payload:, timestamp:, secret:)
  WorkOS::Util::Signature.compute(payload: payload, timestamp: timestamp, secret: secret)
end

#construct_event(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ WorkOS::WebhookEvent

Verify a webhook signature and return a typed event struct.

Parses the ‘WorkOS-Signature` header, validates the HMAC-SHA256 signature against the endpoint secret, checks that the event timestamp is within the tolerance window, and returns a typed WorkOS::WebhookEvent.

Examples:

Rack / Sinatra

post "/webhooks" do
  event = client.webhooks.construct_event(
    payload:   request.body.read,
    sig_header: request.env["HTTP_WORKOS_SIGNATURE"],
    secret:    ENV["WORKOS_WEBHOOK_SECRET"]
  )
  case event.event
  when "user.created" then handle_user_created(event.data)
  end
end

Parameters:

  • payload (String)

    Raw webhook request body (must be the exact bytes received; do not re-serialize).

  • sig_header (String)

    Value of the ‘WorkOS-Signature` header (format: `“t=<ms_timestamp>, v1=<hex_sig>”`).

  • secret (String)

    Webhook endpoint secret from the WorkOS dashboard.

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE_SECONDS)

    Maximum event age in seconds (default: 180). Events older than this are rejected to guard against replay attacks.

Returns:

Raises:



165
166
167
168
# File 'lib/workos/webhooks.rb', line 165

def construct_event(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS)
  raw = verify_event(payload: payload, sig_header: sig_header, secret: secret, tolerance: tolerance)
  WebhookEvent.new(raw)
end

#create_webhook_endpoint(endpoint_url:, events:, request_options: {}) ⇒ WorkOS::WebhookEndpointJson

Create a Webhook Endpoint

Parameters:

  • endpoint_url (String)

    The HTTPS URL where webhooks will be sent.

  • events (Array<WorkOS::Types::CreateWebhookEndpointEvents>)

    The events that the Webhook Endpoint is subscribed to.

  • request_options (Hash) (defaults to: {})

    (see WorkOS::Types::RequestOptions)

Returns:



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/workos/webhooks.rb', line 62

def create_webhook_endpoint(
  endpoint_url:,
  events:,
  request_options: {}
)
  body = {
    "endpoint_url" => endpoint_url,
    "events" => events
  }.compact
  response = @client.request(
    method: :post,
    path: "/webhook_endpoints",
    auth: true,
    body: body,
    request_options: request_options
  )
  result = WorkOS::WebhookEndpointJson.new(response.body)
  result.last_response = WorkOS::Types::ApiResponse.new(http_status: response.code.to_i, http_headers: response.each_header.to_h, request_id: response["x-request-id"])
  result
end

#delete_webhook_endpoint(id:, request_options: {}) ⇒ void

This method returns an undefined value.

Delete a Webhook Endpoint

Parameters:

  • id (String)

    Unique identifier of the Webhook Endpoint.

  • request_options (Hash) (defaults to: {})

    (see WorkOS::Types::RequestOptions)



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

def delete_webhook_endpoint(
  id:,
  request_options: {}
)
  @client.request(
    method: :delete,
    path: "/webhook_endpoints/#{WorkOS::Util.encode_path(id)}",
    auth: true,
    request_options: request_options
  )
  nil
end

#list_webhook_endpoints(before: nil, after: nil, limit: nil, order: "desc", request_options: {}) ⇒ WorkOS::Types::ListStruct<WorkOS::WebhookEndpointJson>

List Webhook Endpoints

Parameters:

  • before (String, nil) (defaults to: nil)

    An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with ‘“obj_123”`, your subsequent call can include `before=“obj_123”` to fetch a new batch of objects before `“obj_123”`.

  • after (String, nil) (defaults to: nil)

    An object ID that defines your place in the list. When the ID is not present, you are at the end of the list. For example, if you make a list request and receive 100 objects, ending with ‘“obj_123”`, your subsequent call can include `after=“obj_123”` to fetch a new batch of objects after `“obj_123”`.

  • limit (Integer, nil) (defaults to: nil)

    Upper limit on the number of objects to return, between ‘1` and `100`.

  • order (WorkOS::Types::WebhooksOrder, nil) (defaults to: "desc")

    Order the results by the creation time. Supported values are ‘“asc”` (ascending), `“desc”` (descending), and `“normal”` (descending with reversed cursor semantics where `before` fetches older records and `after` fetches newer records). Defaults to descending.

  • request_options (Hash) (defaults to: {})

    (see WorkOS::Types::RequestOptions)

Returns:



20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# File 'lib/workos/webhooks.rb', line 20

def list_webhook_endpoints(
  before: nil,
  after: nil,
  limit: nil,
  order: "desc",
  request_options: {}
)
  params = {
    "before" => before,
    "after" => after,
    "limit" => limit,
    "order" => order
  }.compact
  response = @client.request(
    method: :get,
    path: "/webhook_endpoints",
    auth: true,
    params: params,
    request_options: request_options
  )
  fetch_next = ->(cursor) {
    list_webhook_endpoints(
      before: before,
      after: cursor,
      limit: limit,
      order: order,
      request_options: request_options
    )
  }
  WorkOS::Types::ListStruct.from_response(
    response,
    model: WorkOS::WebhookEndpointJson,
    filters: {before: before, limit: limit, order: order},
    fetch_next: fetch_next
  )
end

#parse_signature_header(sig_header) ⇒ Object

Parse a “t=<ms>, v1=<sig>” header into [timestamp, signature]. Exposed publicly for advanced use.



221
222
223
224
225
# File 'lib/workos/webhooks.rb', line 221

def parse_signature_header(sig_header)
  WorkOS::Util::Signature.parse_header(sig_header)
rescue ArgumentError => e
  raise WorkOS::SignatureVerificationError.new(message: e.message, http_status: nil)
end

#update_webhook_endpoint(id:, endpoint_url: nil, status: nil, events: nil, request_options: {}) ⇒ WorkOS::WebhookEndpointJson

Update a Webhook Endpoint

Parameters:

  • id (String)

    Unique identifier of the Webhook Endpoint.

  • endpoint_url (String, nil) (defaults to: nil)

    The HTTPS URL where webhooks will be sent.

  • status (WorkOS::Types::UpdateWebhookEndpointStatus, nil) (defaults to: nil)

    Whether the Webhook Endpoint is enabled or disabled.

  • events (Array<WorkOS::Types::UpdateWebhookEndpointEvents>, nil) (defaults to: nil)

    The events that the Webhook Endpoint is subscribed to.

  • request_options (Hash) (defaults to: {})

    (see WorkOS::Types::RequestOptions)

Returns:



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/workos/webhooks.rb', line 90

def update_webhook_endpoint(
  id:,
  endpoint_url: nil,
  status: nil,
  events: nil,
  request_options: {}
)
  body = {
    "endpoint_url" => endpoint_url,
    "status" => status,
    "events" => events
  }.compact
  response = @client.request(
    method: :patch,
    path: "/webhook_endpoints/#{WorkOS::Util.encode_path(id)}",
    auth: true,
    body: body,
    request_options: request_options
  )
  result = WorkOS::WebhookEndpointJson.new(response.body)
  result.last_response = WorkOS::Types::ApiResponse.new(http_status: response.code.to_i, http_headers: response.each_header.to_h, request_id: response["x-request-id"])
  result
end

#verify_event(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ Hash

Verify a webhook signature and return the deserialized event payload.

Parameters:

  • payload (String)

    Raw webhook request body.

  • sig_header (String)

    Value of the WorkOS-Signature header (format: “t=<ms_timestamp>, v1=<hex_sig>”).

  • secret (String)

    Webhook endpoint secret from the dashboard.

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE_SECONDS)

    Maximum age of the event in seconds.

Returns:

  • (Hash)

    Parsed JSON payload.

Raises:



179
180
181
182
# File 'lib/workos/webhooks.rb', line 179

def verify_event(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS)
  verify_header(payload: payload, sig_header: sig_header, secret: secret, tolerance: tolerance)
  JSON.parse(payload)
end

#verify_header(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS) ⇒ true

Verify the webhook signature without deserializing the payload.

Parameters:

  • payload (String)

    Raw webhook request body.

  • sig_header (String)

    Value of the ‘WorkOS-Signature` header.

  • secret (String)

    Webhook endpoint secret.

  • tolerance (Integer) (defaults to: DEFAULT_TOLERANCE_SECONDS)

    Maximum event age in seconds (default: 180).

Returns:

  • (true)

    Returns ‘true` on success.

Raises:



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/workos/webhooks.rb', line 192

def verify_header(payload:, sig_header:, secret:, tolerance: DEFAULT_TOLERANCE_SECONDS)
  timestamp_ms, signature_hash = parse_signature_header(sig_header)
  max_age = tolerance.to_i
  issued_at = timestamp_ms.to_i / 1000.0
  if (Time.now.to_f - issued_at) > max_age
    raise WorkOS::SignatureVerificationError.new(
      message: "Timestamp outside the tolerance zone",
      http_status: nil
    )
  end

  expected = compute_signature(payload: payload, timestamp: timestamp_ms, secret: secret)
  unless secure_compare(signature_hash, expected)
    raise WorkOS::SignatureVerificationError.new(
      message: "Signature hash does not match the expected signature hash for payload",
      http_status: nil
    )
  end
  true
end