Class: Sendgrid::SignatureVerifier
- Inherits:
-
Object
- Object
- Sendgrid::SignatureVerifier
- Defined in:
- app/services/sendgrid/signature_verifier.rb
Overview
Verifies SendGrid Event Webhook signatures using ECDSA
Reference: https://www.twilio.com/docs/sendgrid/for-developers/tracking-events/getting-started-event-webhook-security-features
When signature verification is enabled in SendGrid:
- SendGrid generates a public/private key pair
- Each webhook POST includes:
- X-Twilio-Email-Event-Webhook-Signature: Base64-encoded ECDSA signature
- X-Twilio-Email-Event-Webhook-Timestamp: Unix timestamp
- The signature is computed over timestamp + raw payload
Multiple SendGrid Subusers:
- Each subuser (warmlyyours, warmlyyours_transaction, warmlyyours_marketing) has its own key
- Pass the subuser name to look up the correct key from credentials
- Keys stored at: sendgrid_api..webhook_verification_key
Constant Summary collapse
- SIGNATURE_HEADER =
Header names as they appear in Rack env
'HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_SIGNATURE'- TIMESTAMP_HEADER =
'HTTP_X_TWILIO_EMAIL_EVENT_WEBHOOK_TIMESTAMP'- DEFAULT_SUBUSER =
Default subuser if none specified
'warmlyyours'
Instance Attribute Summary collapse
-
#subuser ⇒ Object
readonly
Returns the value of attribute subuser.
Instance Method Summary collapse
-
#enabled? ⇒ Boolean
Check if signature verification is enabled.
-
#fetch_public_key ⇒ Object
Fetch the public key for the current subuser from credentials Priority: ENV > credentials for specific subuser.
-
#initialize(public_key: nil, subuser: nil) ⇒ SignatureVerifier
constructor
A new instance of SignatureVerifier.
-
#verify(request) ⇒ Boolean
Verify a request's signature.
-
#verify_signature(payload, signature, timestamp) ⇒ Boolean
Verify a signature directly.
Constructor Details
#initialize(public_key: nil, subuser: nil) ⇒ SignatureVerifier
Returns a new instance of SignatureVerifier.
43 44 45 46 |
# File 'app/services/sendgrid/signature_verifier.rb', line 43 def initialize(public_key: nil, subuser: nil) @subuser = subuser.presence || DEFAULT_SUBUSER @public_key = public_key || fetch_public_key end |
Instance Attribute Details
#subuser ⇒ Object (readonly)
Returns the value of attribute subuser.
41 42 43 |
# File 'app/services/sendgrid/signature_verifier.rb', line 41 def subuser @subuser end |
Instance Method Details
#enabled? ⇒ Boolean
Check if signature verification is enabled
60 61 62 |
# File 'app/services/sendgrid/signature_verifier.rb', line 60 def enabled? @public_key.present? end |
#fetch_public_key ⇒ Object
Fetch the public key for the current subuser from credentials
Priority: ENV > credentials for specific subuser
50 51 52 53 54 55 56 |
# File 'app/services/sendgrid/signature_verifier.rb', line 50 def fetch_public_key # First check ENV (useful for testing or overriding) return ENV['SENDGRID_WEBHOOK_VERIFICATION_KEY'] if ENV['SENDGRID_WEBHOOK_VERIFICATION_KEY'].present? # Look up from credentials: sendgrid_api.<subuser>.webhook_verification_key Heatwave::Configuration.fetch(:sendgrid_api, @subuser.to_sym, :webhook_verification_key) end |
#verify(request) ⇒ Boolean
Verify a request's signature
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 |
# File 'app/services/sendgrid/signature_verifier.rb', line 67 def verify(request) return true unless enabled? signature = request.headers[SIGNATURE_HEADER] || request.env[SIGNATURE_HEADER] || request.headers['X-Twilio-Email-Event-Webhook-Signature'] = request.headers[TIMESTAMP_HEADER] || request.env[TIMESTAMP_HEADER] || request.headers['X-Twilio-Email-Event-Webhook-Timestamp'] payload = request.raw_post if signature.blank? || .blank? log_verification_failure('Missing signature or timestamp header', request) return false end result = verify_signature(payload, signature, ) log_verification_failure('Invalid signature', request) unless result result end |
#verify_signature(payload, signature, timestamp) ⇒ Boolean
Verify a signature directly
95 96 97 98 99 100 101 102 103 104 105 106 107 108 |
# File 'app/services/sendgrid/signature_verifier.rb', line 95 def verify_signature(payload, signature, ) return false if @public_key.blank? event_webhook = SendGrid::EventWebhook.new ec_public_key = event_webhook.convert_public_key_to_ecdsa(@public_key) event_webhook.verify_signature(ec_public_key, payload, signature, ) rescue SendGrid::EventWebhook::NotSupportedError => e Rails.logger.error "[SendGrid Signature] #{e.}" # If verification isn't supported (JRuby), allow the request but log it true rescue StandardError => e Rails.logger.error "[SendGrid Signature] Verification error: #{e.}" false end |