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
  }
  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: 10, 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: 10)

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

  • order (WorkOS::Types::PaginationOrder, 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: 10,
  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=, v1=" 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