mirror of https://github.com/docusealco/docuseal
parent
1304849b55
commit
45ae954c0c
@ -0,0 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class WebhookHmacController < ApplicationController
|
||||
load_and_authorize_resource :webhook_url, parent: false
|
||||
|
||||
def show; end
|
||||
end
|
||||
@ -0,0 +1,20 @@
|
||||
<%= render 'shared/turbo_modal', title: t('webhook_security') do %>
|
||||
<div class="text-center mb-4">
|
||||
<div class="inline-flex justify-center">
|
||||
<%= link_to t('secret'), webhook_secret_path(@webhook_url), class: 'block bg-base-200 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-l-3xl', data: { turbo_frame: 'modal' } %>
|
||||
<%= link_to t('hmac'), webhook_hmac_path(@webhook_url), class: 'block bg-base-300 md:min-w-[112px] text-sm font-semibold whitespace-nowrap py-1.5 px-4 rounded-r-3xl', data: { turbo_frame: 'modal' } %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="hmac_secret"><%= t('hmac_signing_secret') %></label>
|
||||
<% token = @webhook_url.hmac_secret %>
|
||||
<% obscured = "#{token[0, 10]}#{'*' * [token.length - 10, 0].max}" %>
|
||||
<div class="flex gap-2">
|
||||
<masked-input class="block w-full" data-token="<%= token %>">
|
||||
<input id="hmac_secret" type="text" value="<%= obscured %>" class="base-input font-mono w-full" autocomplete="off" readonly>
|
||||
</masked-input>
|
||||
<%= render 'shared/clipboard_copy', icon: 'copy', text: token, class: 'base-button', icon_class: 'w-6 h-6 text-white', copy_title: t('copy'), copied_title: t('copied') %>
|
||||
</div>
|
||||
<p class="text-sm mt-2 opacity-70"><%= t('hmac_signature_header_hint_html', header: '<code>X-Docuseal-Signature</code>'.html_safe) %></p>
|
||||
</div>
|
||||
<% end %>
|
||||
@ -0,0 +1,23 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
class AddHmacToWebhookUrls < ActiveRecord::Migration[8.1]
|
||||
class MigrationWebhookUrl < ApplicationRecord
|
||||
self.table_name = 'webhook_urls'
|
||||
|
||||
encrypts :hmac_secret
|
||||
end
|
||||
|
||||
def up
|
||||
add_column :webhook_urls, :hmac_secret, :text
|
||||
|
||||
MigrationWebhookUrl.find_each do |webhook_url|
|
||||
webhook_url.update_columns(hmac_secret: WebhookUrls::Signatures.generate_secret)
|
||||
end
|
||||
|
||||
change_column_null :webhook_urls, :hmac_secret, false
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :webhook_urls, :hmac_secret
|
||||
end
|
||||
end
|
||||
@ -0,0 +1,40 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module WebhookUrls
|
||||
module Signatures
|
||||
SECRET_PREFIX = 'whsec_'
|
||||
SECRET_BYTES = 24
|
||||
TOLERANCE = 5 * 60
|
||||
|
||||
InvalidSignatureError = Class.new(StandardError)
|
||||
TimestampError = Class.new(StandardError)
|
||||
|
||||
module_function
|
||||
|
||||
def generate_secret
|
||||
SECRET_PREFIX + Base64.strict_encode64(SecureRandom.bytes(SECRET_BYTES))
|
||||
end
|
||||
|
||||
def sign(secret, body:, timestamp: Time.current.to_i)
|
||||
"#{timestamp}.#{OpenSSL::HMAC.hexdigest('sha256', secret, "#{timestamp}.#{body}")}"
|
||||
end
|
||||
|
||||
def verify(secret, body:, header:, tolerance: TOLERANCE)
|
||||
ts, sig = header.to_s.split('.', 2)
|
||||
ts = Integer(ts, exception: false)
|
||||
|
||||
raise InvalidSignatureError unless ts && sig
|
||||
|
||||
now = Time.current.to_i
|
||||
|
||||
raise TimestampError, 'Too old' if ts < now - tolerance
|
||||
raise TimestampError, 'In future' if ts > now + tolerance
|
||||
|
||||
expected = OpenSSL::HMAC.hexdigest('sha256', secret, "#{ts}.#{body}")
|
||||
|
||||
raise InvalidSignatureError unless ActiveSupport::SecurityUtils.secure_compare(expected, sig)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in new issue