bullet_train-outgoing_webhooks 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/account/webhooks/outgoing/endpoints_controller.rb +11 -0
- data/app/models/concerns/webhooks/outgoing/delivery_attempt_support.rb +13 -1
- data/app/models/concerns/webhooks/outgoing/endpoint_support.rb +10 -0
- data/app/views/account/webhooks/outgoing/endpoints/_form.html.erb +1 -0
- data/app/views/account/webhooks/outgoing/endpoints/_index.html.erb +2 -0
- data/app/views/account/webhooks/outgoing/endpoints/show.html.erb +9 -0
- data/app/views/api/v1/webhooks/outgoing/endpoints/_endpoint.json.jbuilder +5 -0
- data/config/locales/en/webhooks/outgoing/endpoints.en.yml +16 -2
- data/config/routes.rb +4 -0
- data/lib/bullet_train/outgoing_webhooks/engine.rb +3 -1
- data/lib/bullet_train/outgoing_webhooks/signature.rb +68 -0
- data/lib/bullet_train/outgoing_webhooks/version.rb +1 -1
- data/lib/bullet_train/outgoing_webhooks.rb +1 -0
- metadata +16 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dba0cb965743f696358e08d3f2bcfefbec7598b4de625b3315469d030a2be551
|
4
|
+
data.tar.gz: 9d33299349ba554bb4d409c39c9831cbbdc1355414f621068c623d6734d360e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7f09f96deff0e056aa9d6e3732e4dba9c407f6c8adbf7ffa627d13f0d94f3e6db4d32a73efd836331b93aed07314f3d7176ff824e0657bc337c522e9dfa6721
|
7
|
+
data.tar.gz: 4e4754e98e1e5003ed4ee4ee656368a08fd27cf0e327f332cf191a36e2b1f9056bb327dd12d23e64bac6d61457f6866cbf02e5bf16b0a6186c2f864fcca67d84
|
@@ -60,6 +60,17 @@ class Account::Webhooks::Outgoing::EndpointsController < Account::ApplicationCon
|
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
63
|
+
# POST /account/webhooks/outgoing/endpoints/:id/rotate_secret
|
64
|
+
def rotate_secret
|
65
|
+
@endpoint ||= Webhooks::Outgoing::Endpoint.accessible_by(current_ability).find(params[:id])
|
66
|
+
|
67
|
+
respond_to do |format|
|
68
|
+
if @endpoint.rotate_webhook_secret!
|
69
|
+
format.html { redirect_to [:account, @endpoint], notice: I18n.t("webhooks/outgoing/endpoints.notifications.secret_rotated") }
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
63
74
|
private
|
64
75
|
|
65
76
|
# Never trust parameters from the scary internet, only allow the white list through.
|
@@ -59,10 +59,22 @@ module Webhooks::Outgoing::DeliveryAttemptSupport
|
|
59
59
|
http.verify_mode = BulletTrain::OutgoingWebhooks.http_verify_mode
|
60
60
|
end
|
61
61
|
end
|
62
|
+
|
62
63
|
request = Net::HTTP::Post.new(uri.request_uri)
|
63
64
|
request.add_field("Host", uri.host)
|
64
65
|
request.add_field("Content-Type", "application/json")
|
65
|
-
|
66
|
+
|
67
|
+
# Generate and add signature headers
|
68
|
+
payload = delivery.event.payload
|
69
|
+
signature_data = BulletTrain::OutgoingWebhooks::Signature
|
70
|
+
.generate(payload, delivery.endpoint.webhook_secret)
|
71
|
+
|
72
|
+
webhook_headers_namespace = Rails.configuration.outgoing_webhooks[:webhook_headers_namespace]
|
73
|
+
request.add_field("#{webhook_headers_namespace}-Signature", signature_data[:signature])
|
74
|
+
request.add_field("#{webhook_headers_namespace}-Timestamp", signature_data[:timestamp])
|
75
|
+
request.add_field("#{webhook_headers_namespace}-Id", delivery.event.uuid)
|
76
|
+
|
77
|
+
request.body = payload.to_json
|
66
78
|
|
67
79
|
begin
|
68
80
|
response = http.request(request)
|
@@ -14,8 +14,10 @@ module Webhooks::Outgoing::EndpointSupport
|
|
14
14
|
validates :name, presence: true
|
15
15
|
|
16
16
|
before_validation { url&.strip! }
|
17
|
+
before_validation :generate_webhook_secret, on: :create
|
17
18
|
|
18
19
|
validates :url, presence: true, allowed_uri: BulletTrain::OutgoingWebhooks.advanced_hostname_security
|
20
|
+
validates :webhook_secret, presence: true
|
19
21
|
|
20
22
|
after_initialize do
|
21
23
|
self.event_type_ids ||= []
|
@@ -40,4 +42,12 @@ module Webhooks::Outgoing::EndpointSupport
|
|
40
42
|
def touch_parent
|
41
43
|
send(BulletTrain::OutgoingWebhooks.parent_association).touch
|
42
44
|
end
|
45
|
+
|
46
|
+
def generate_webhook_secret
|
47
|
+
self.webhook_secret ||= SecureRandom.hex(32)
|
48
|
+
end
|
49
|
+
|
50
|
+
def rotate_webhook_secret!
|
51
|
+
update!(webhook_secret: SecureRandom.hex(32))
|
52
|
+
end
|
43
53
|
end
|
@@ -15,6 +15,7 @@
|
|
15
15
|
<tr>
|
16
16
|
<th><%= t('.fields.name.heading') %></th>
|
17
17
|
<th><%= t('.fields.url.heading') %></th>
|
18
|
+
<th><%= t('.fields.webhook_secret.heading') %></th>
|
18
19
|
<%# 🚅 super scaffolding will insert new field headers above this line. %>
|
19
20
|
<th class="text-right"></th>
|
20
21
|
</tr>
|
@@ -25,6 +26,7 @@
|
|
25
26
|
<tr data-id="<%= endpoint.id %>">
|
26
27
|
<td><%= render 'shared/attributes/text', attribute: :name, url: [:account, endpoint] %></td>
|
27
28
|
<td><%= render 'shared/attributes/code', attribute: :url %></td>
|
29
|
+
<td><%= render 'shared/attributes/code', attribute: :webhook_secret, secret: true %></td>
|
28
30
|
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
29
31
|
<td class="buttons">
|
30
32
|
<% unless hide_actions %>
|
@@ -28,6 +28,7 @@
|
|
28
28
|
<% end %>
|
29
29
|
|
30
30
|
<%= render 'shared/attributes/belongs_to', attribute: :scaffolding_absolutely_abstract_creative_concept %>
|
31
|
+
<%= render 'shared/attributes/code', attribute: :webhook_secret, secret: true %>
|
31
32
|
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
32
33
|
<% end %>
|
33
34
|
<% end %>
|
@@ -36,6 +37,14 @@
|
|
36
37
|
<%= link_to t('.buttons.edit'), [:edit, :account, @endpoint], class: first_button_primary if can? :edit, @endpoint %>
|
37
38
|
<%= button_to t('.buttons.destroy'), [:account, @endpoint], method: :delete, class: first_button_primary, data: { turbo_confirm: t('.buttons.confirmations.destroy', model_locales(@endpoint)) } if can? :destroy, @endpoint %>
|
38
39
|
<%= link_to t('global.buttons.back'), [:account, @parent, :webhooks_outgoing_endpoints], class: first_button_primary %>
|
40
|
+
<% if can? :edit, @endpoint %>
|
41
|
+
<%=
|
42
|
+
button_to t('.buttons.rotate_secret'),
|
43
|
+
rotate_secret_account_webhooks_outgoing_endpoint_path(@endpoint),
|
44
|
+
method: :post, class: first_button_primary,
|
45
|
+
data: { turbo_confirm: t('.buttons.confirmations.rotate_secret', model_locales(@endpoint)) }
|
46
|
+
%>
|
47
|
+
<% end %>
|
39
48
|
<% end %>
|
40
49
|
<% end %>
|
41
50
|
|
@@ -7,3 +7,8 @@ json.extract! endpoint,
|
|
7
7
|
# 🚅 super scaffolding will insert new fields above this line.
|
8
8
|
:created_at,
|
9
9
|
:updated_at
|
10
|
+
|
11
|
+
# Avoid spilling secrets via the API. We still need to show it once on create so
|
12
|
+
# endpoints created programmaticly via the API can save it and use it to verify
|
13
|
+
# the signature.
|
14
|
+
json.webhook_secret endpoint.webhook_secret if endpoint.previously_new_record?
|
@@ -12,12 +12,15 @@ en:
|
|
12
12
|
edit: Edit Settings
|
13
13
|
update: Update Endpoint
|
14
14
|
destroy: Remove Endpoint
|
15
|
+
rotate_secret: Rotate Secret
|
15
16
|
shorthand:
|
16
17
|
edit: Settings
|
17
18
|
destroy: Delete
|
18
19
|
confirmations:
|
19
|
-
# TODO customize for your use-case.
|
20
20
|
destroy: Are you sure you want to remove %{endpoint_name}? This will also remove it's associated data. This can't be undone.
|
21
|
+
rotate_secret: >
|
22
|
+
Are you sure you want to rotate the secret of the endpoint %{endpoint_name}?
|
23
|
+
Your webhook event verification system will need to start handling the new secret.
|
21
24
|
fields: &fields
|
22
25
|
id:
|
23
26
|
_: &id Endpoint ID
|
@@ -74,6 +77,15 @@ en:
|
|
74
77
|
|
75
78
|
scaffolding_absolutely_abstract_creative_concept: *scaffolding_absolutely_abstract_creative_concept
|
76
79
|
|
80
|
+
webhook_secret:
|
81
|
+
_: &webhook_secret Webhook Secret
|
82
|
+
label: *webhook_secret
|
83
|
+
heading: *webhook_secret
|
84
|
+
api_description: |
|
85
|
+
Each webhook endpoint has a rollable secret. If you create your endpoints programmatically
|
86
|
+
via the API, the secret is only shown after creating the endpoint. If you missed to store
|
87
|
+
it, you will need to look it up on the app's UI endpoint show or index pages.
|
88
|
+
truncation_with_instructions: ... (use button below to rotate)
|
77
89
|
# 🚅 super scaffolding will insert new fields above this line.
|
78
90
|
created_at:
|
79
91
|
_: &created_at Added
|
@@ -130,6 +142,7 @@ en:
|
|
130
142
|
created: Endpoint was successfully created.
|
131
143
|
updated: Endpoint was successfully updated.
|
132
144
|
destroyed: Endpoint was successfully destroyed.
|
145
|
+
secret_rotated: Endpoint's webhook secret successfully rotated.
|
133
146
|
account:
|
134
147
|
webhooks:
|
135
148
|
outgoing:
|
@@ -142,6 +155,7 @@ en:
|
|
142
155
|
api_version: *api_version
|
143
156
|
event_type_ids: *event_type_ids
|
144
157
|
scaffolding_absolutely_abstract_creative_concept_id: *scaffolding_absolutely_abstract_creative_concept_id
|
158
|
+
webhook_secret: *webhook_secret
|
145
159
|
# 🚅 super scaffolding will insert new activerecord attributes above this line.
|
146
160
|
created_at: *created_at
|
147
|
-
updated_at: *updated_at
|
161
|
+
updated_at: *updated_at
|
data/config/routes.rb
CHANGED
@@ -22,7 +22,9 @@ module BulletTrain
|
|
22
22
|
allowed_schemes: %w[http https],
|
23
23
|
custom_block_callback: nil,
|
24
24
|
custom_allow_callback: nil,
|
25
|
-
audit_callback: ->(obj, uri) { Rails.logger.error("BlockedURI obj=#{obj.persisted? ? obj.to_global_id : "New #{obj.class}"} uri=#{uri}") }
|
25
|
+
audit_callback: ->(obj, uri) { Rails.logger.error("BlockedURI obj=#{obj.persisted? ? obj.to_global_id : "New #{obj.class}"} uri=#{uri}") },
|
26
|
+
webhook_headers_namespace: "X-Webhook-Bullet-Train",
|
27
|
+
event_verification_tolerance_seconds: 600 # 5-10 minutes is industry-default.
|
26
28
|
}
|
27
29
|
end
|
28
30
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module BulletTrain
|
2
|
+
module OutgoingWebhooks
|
3
|
+
# Provides methods for webhook signatures. This module also serves as an
|
4
|
+
# example that can be used by receiving applications to verify webhook
|
5
|
+
# authenticity.
|
6
|
+
module Signature
|
7
|
+
# Verifies the authenticity of a webhook request.
|
8
|
+
#
|
9
|
+
# @param payload [String] The raw request body as a string.
|
10
|
+
# @param timestamp [String] The timestamp from the Timestamp request header.
|
11
|
+
# @param signature [String] The signature from the Signature request header.
|
12
|
+
# @param secret [String] The webhook secret attached to the endpoint the event comes from.
|
13
|
+
# @return [Boolean] True if the signature is valid, false otherwise.
|
14
|
+
def self.verify(payload, signature, timestamp, secret)
|
15
|
+
return false if payload.blank? || signature.blank? || timestamp.blank? || secret.blank?
|
16
|
+
|
17
|
+
tolerance = Rails.configuration.outgoing_webhooks[:event_verification_tolerance_seconds]
|
18
|
+
# Check if the timestamp is too old
|
19
|
+
timestamp_int = timestamp.to_i
|
20
|
+
now = Time.now.to_i
|
21
|
+
|
22
|
+
if (now - timestamp_int).abs > tolerance
|
23
|
+
return false # Webhook is too old or timestamp is from the future
|
24
|
+
end
|
25
|
+
|
26
|
+
# Compute the expected signature
|
27
|
+
signature_payload = "#{timestamp}.#{payload}"
|
28
|
+
expected_signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_payload)
|
29
|
+
|
30
|
+
# Compare signatures using constant-time comparison to prevent timing attacks
|
31
|
+
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
|
32
|
+
end
|
33
|
+
|
34
|
+
# A Rails controller helper example to verify webhook requests.
|
35
|
+
#
|
36
|
+
# @param request [ActionDispatch::Request] The Rails request object.
|
37
|
+
# @param secret [String] The webhook secret shared with the sender.
|
38
|
+
# @return [Boolean] True if the signature is valid, false otherwise.
|
39
|
+
def self.verify_request(request, secret)
|
40
|
+
return false if request.blank? || secret.blank?
|
41
|
+
|
42
|
+
webhook_headers_namespace = Rails.configuration.outgoing_webhooks[:webhook_headers_namespace]
|
43
|
+
signature = request.headers["#{webhook_headers_namespace}-Signature"]
|
44
|
+
timestamp = request.headers["#{webhook_headers_namespace}-Timestamp"]
|
45
|
+
payload = request.raw_post
|
46
|
+
|
47
|
+
return false if signature.blank? || timestamp.blank?
|
48
|
+
|
49
|
+
verify(payload, signature, timestamp, secret)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Algorithm to generate the signature.
|
53
|
+
#
|
54
|
+
# @payload [Hash] The payload to be encoded into a signature.
|
55
|
+
# @secret [String] The secret stored on each webhook endpoint.
|
56
|
+
def self.generate(payload, secret)
|
57
|
+
timestamp = Time.now.to_i.to_s
|
58
|
+
signature_payload = "#{timestamp}.#{payload.to_json}"
|
59
|
+
signature = OpenSSL::HMAC.hexdigest("SHA256", secret, signature_payload)
|
60
|
+
|
61
|
+
{
|
62
|
+
signature: signature,
|
63
|
+
timestamp: timestamp
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bullet_train-outgoing_webhooks
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.25.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Culver
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - ">="
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '0'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: webmock
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
54
68
|
- !ruby/object:Gem::Dependency
|
55
69
|
name: rails
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -182,6 +196,7 @@ files:
|
|
182
196
|
- db/migrate/20221231003438_remove_default_from_webhooks_outgoing_events.rb
|
183
197
|
- lib/bullet_train/outgoing_webhooks.rb
|
184
198
|
- lib/bullet_train/outgoing_webhooks/engine.rb
|
199
|
+
- lib/bullet_train/outgoing_webhooks/signature.rb
|
185
200
|
- lib/bullet_train/outgoing_webhooks/version.rb
|
186
201
|
- lib/tasks/bullet_train/outgoing_webhooks_tasks.rake
|
187
202
|
homepage: https://github.com/bullet-train-co/bullet_train-core/tree/main/bullet_train-outgoing_webhooks
|