Skip to content

Commit b36e471

Browse files
author
marcel corso gonzalez
authored
Merge pull request #68 from nqkdev/master
Add support for new signature method
2 parents e3c703f + 6589a60 commit b36e471

File tree

8 files changed

+603
-44
lines changed

8 files changed

+603
-44
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
$LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + '/../lib/')
5+
require 'messagebird'
6+
7+
SIGNING_KEY = 'PlLrKaqvZNRR5zAjm42ZT6q1SQxgbbGd'
8+
9+
url = 'https://FULL_REQUEST_URL/path?query=1'
10+
signature = 'YOUR_REQUEST_SIGNATURE'
11+
body = ''
12+
13+
request_validator = MessageBird::RequestValidator.new(SIGNING_KEY)
14+
15+
begin
16+
# Verify the signed request.
17+
request_validator.validate_signature(signature, url, body.bytes.to_a)
18+
rescue MessageBird::ValidationError => e
19+
puts
20+
puts 'An error occurred while verifying the signed request:'
21+
puts e
22+
end

examples/signed_request_verification.rb

Lines changed: 0 additions & 43 deletions
This file was deleted.

lib/messagebird.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
require 'messagebird/hlr'
1313
require 'messagebird/http_client'
1414
require 'messagebird/message_reference'
15-
require 'messagebird/signed_request'
15+
require 'messagebird/signed_request' # @deprecated
16+
require 'messagebird/request_validator'
1617
require 'messagebird/verify'
1718
require 'messagebird/message'
1819
require 'messagebird/voicemessage'
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# frozen_string_literal: true
2+
3+
require 'base64'
4+
require 'digest'
5+
require 'time'
6+
require 'jwt'
7+
8+
module MessageBird
9+
class ValidationError < StandardError
10+
end
11+
12+
##
13+
# RequestValidator validates request signature signed by MessageBird services.
14+
#
15+
# @see https://developers.messagebird.com/docs/verify-http-requests
16+
class RequestValidator
17+
ALLOWED_ALGOS = %w[HS256 HS384 HS512].freeze
18+
19+
##
20+
#
21+
# @param [string] signature_key customer signature key. Can be retrieved through <a href="https://dashboard.messagebird.com/developers/settings">Developer Settings</a>. This is NOT your API key.
22+
# @param [bool] skip_url_validation whether url_hash claim validation should be skipped. Note that when true, no query parameters should be trusted.
23+
def initialize(signature_key, skip_url_validation = false)
24+
@signature_key = signature_key
25+
@skip_url_validation = skip_url_validation
26+
end
27+
28+
##
29+
# This method validates provided request signature, which is a JWT token.
30+
# This JWT is signed with a MessageBird account unique secret key, ensuring the request is from MessageBird and a specific account.
31+
# The JWT contains the following claims:
32+
# * "url_hash" - the raw URL hashed with SHA256 ensuring the URL wasn't altered.
33+
# * "payload_hash" - the raw payload hashed with SHA256 ensuring the payload wasn't altered.
34+
# * "jti" - a unique token ID to implement an optional non-replay check (NOT validated by default).
35+
# * "nbf" - the not before timestamp.
36+
# * "exp" - the expiration timestamp is ensuring that a request isn't captured and used at a later time.
37+
# * "iss" - the issuer name, always MessageBird.
38+
# @param [String] signature the actual signature taken from request header "MessageBird-Signature-JWT".
39+
# @param [String] url the raw url including the protocol, hostname and query string, e.g. "https://example.com/?example=42".
40+
# @param [Array] request_body the raw request body.
41+
# @return [Array] raw signature payload
42+
# @raise [ValidationError] if signature is invalid
43+
# @see https://developers.messagebird.com/docs/verify-http-requests
44+
def validate_signature(signature, url, request_body)
45+
raise ValidationError, 'Signature can not be empty' if signature.to_s.empty?
46+
raise ValidationError, 'URL can not be empty' if !@skip_url_validation && url.to_s.empty?
47+
48+
claims = decode_signature signature
49+
validate_url(url, claims['url_hash']) unless @skip_url_validation
50+
validate_payload(request_body, claims['payload_hash'])
51+
52+
claims
53+
end
54+
55+
private # Applies to every method below this line
56+
57+
def decode_signature(signature)
58+
begin
59+
claims, * = JWT.decode signature, @signature_key, true,
60+
algorithm: ALLOWED_ALGOS,
61+
iss: 'MessageBird',
62+
required_claims: %w[iss nbf exp],
63+
verify_iss: true,
64+
leeway: 1
65+
rescue JWT::DecodeError => e
66+
raise ValidationError, e
67+
end
68+
69+
claims
70+
end
71+
72+
def validate_url(url, url_hash)
73+
expected_url_hash = Digest::SHA256.hexdigest url
74+
unless JWT::SecurityUtils.secure_compare(expected_url_hash, url_hash)
75+
raise ValidationError, 'invalid jwt: claim url_hash is invalid'
76+
end
77+
end
78+
79+
def validate_payload(body, payload_hash)
80+
if !body.to_s.empty? && !payload_hash.to_s.empty?
81+
unless JWT::SecurityUtils.secure_compare(Digest::SHA256.hexdigest(body), payload_hash)
82+
raise ValidationError, 'invalid jwt: claim payload_hash is invalid'
83+
end
84+
elsif !body.to_s.empty?
85+
raise ValidationError, 'invalid jwt: claim payload_hash is not set but payload is present'
86+
elsif !payload_hash.to_s.empty?
87+
raise ValidationError, 'invalid jwt: claim payload_hash is set but actual payload is missing'
88+
end
89+
end
90+
end
91+
end

lib/messagebird/signed_request.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@
55
require 'time'
66

77
module MessageBird
8+
##
9+
# @deprecated Use {MessageBird::RequestValidator::ValidationError} instead.
810
class ValidationException < TypeError
911
end
1012

13+
##
14+
# @deprecated Use {MessageBird::RequestValidator} instead.
1115
class SignedRequest
16+
##
17+
# @deprecated Use {MessageBird::RequestValidator} instead.
1218
def initialize(query_parameters, signature, request_timestamp, body)
1319
unless query_parameters.is_a? Hash
1420
raise ValidationException, 'The "query_parameters" value is invalid.'
@@ -29,12 +35,16 @@ def initialize(query_parameters, signature, request_timestamp, body)
2935
@body = body
3036
end
3137

38+
##
39+
# @deprecated Use {MessageBird::RequestValidator::validateSignature} instead.
3240
def verify(signing_key)
3341
calculated_signature = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), signing_key, build_payload)
3442
expected_signature = Base64.decode64(@signature)
3543
calculated_signature.bytes == expected_signature.bytes
3644
end
3745

46+
##
47+
# @deprecated Use {MessageBird::RequestValidator} instead.
3848
def build_payload
3949
parts = []
4050
parts.push(@request_timestamp)
@@ -43,6 +53,8 @@ def build_payload
4353
parts.join("\n")
4454
end
4555

56+
##
57+
# @deprecated Use {MessageBird::RequestValidator} instead.
4658
def recent?(offset = 10)
4759
(Time.now.getutc.to_i - @request_timestamp) < offset
4860
end

messagebird-rest.gemspec

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Gem::Specification.new do |s|
2323
s.files = Dir.glob('lib/**/*') + %w(LICENSE README.md)
2424
s.require_path = 'lib'
2525

26+
s.add_dependency "jwt", "~> 2.2.3"
27+
2628
s.add_development_dependency "rspec", "~> 3.10.0"
2729
s.add_development_dependency "rubocop", "~> 0.77.0"
2830
s.add_development_dependency 'webmock', '~> 3.7.5'

0 commit comments

Comments
 (0)