api_signature 0.1.5 → 1.0.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 +5 -5
- data/.rubocop.yml +1 -12
- data/.ruby-version +1 -0
- data/README.md +62 -71
- data/api_signature.gemspec +0 -4
- data/lib/api_signature.rb +17 -9
- data/lib/api_signature/auth_header.rb +61 -0
- data/lib/api_signature/builder.rb +95 -27
- data/lib/api_signature/configuration.rb +14 -0
- data/lib/api_signature/signature.rb +34 -0
- data/lib/api_signature/signer.rb +150 -0
- data/lib/api_signature/spec_support/helper.rb +8 -3
- data/lib/api_signature/utils.rb +120 -0
- data/lib/api_signature/validator.rb +75 -16
- data/lib/api_signature/version.rb +1 -1
- metadata +9 -48
- data/lib/api_signature/generator.rb +0 -42
- data/lib/api_signature/request.rb +0 -48
- data/lib/api_signature/spec_support/headers_builder.rb +0 -39
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f97c890250e1a784f8e590ec6fc803e23c15a77ad9d2cb374f2418d629aacb1f
|
4
|
+
data.tar.gz: a7ed8c86f5d82b7b5e8577df62a150cdbfd9ec9c63c831f70e9315c2e4cf2dc2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c1a20884bdb0b2c91bc6a6ecfc40354e0fa89ea11badcea7445b3b95bd309a13c35d2e301bee619cffbe431b32e77447d44b2f46ff3c3f5e645473b890999177
|
7
|
+
data.tar.gz: 021a30a6039154f1509235aa5d01cd666d101b1ee617a1e828ceb2de11b25bb5422ecfe44bec314062aaa947b73c32839487e776f75e2c48004a5726732d109a
|
data/.rubocop.yml
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion: 2.
|
2
|
+
TargetRubyVersion: 2.5.5
|
3
3
|
Exclude:
|
4
4
|
- "bin/*"
|
5
5
|
- "Guardfile"
|
@@ -32,14 +32,3 @@ Metrics/BlockLength:
|
|
32
32
|
|
33
33
|
Metrics/MethodLength:
|
34
34
|
Max: 15
|
35
|
-
|
36
|
-
##################### Rails ##################################
|
37
|
-
|
38
|
-
Rails:
|
39
|
-
Enabled: true
|
40
|
-
|
41
|
-
Rails/SkipsModelValidations:
|
42
|
-
Enabled: false
|
43
|
-
|
44
|
-
Rails/InverseOf:
|
45
|
-
Enabled: false
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.5
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
# ApiSignature
|
6
6
|
|
7
|
-
Simple HMAC-SHA1 authentication via headers
|
7
|
+
Simple HMAC-SHA1 authentication via headers. Impressed by [AWS Requests with Signature Version 4](https://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html)
|
8
8
|
|
9
9
|
This gem will generate signature for the client requests and verify that signature on the server side
|
10
10
|
|
@@ -16,101 +16,92 @@ Add this line to your application's Gemfile:
|
|
16
16
|
gem 'api_signature'
|
17
17
|
```
|
18
18
|
|
19
|
-
And then execute:
|
20
|
-
|
21
|
-
$ bundle
|
22
|
-
|
23
19
|
## Usage
|
24
20
|
|
25
|
-
|
26
|
-
|
27
|
-
Implement warden strategy:
|
28
|
-
```ruby
|
29
|
-
module MyApplication
|
30
|
-
module API
|
31
|
-
class ClientAuthenticatable < Warden::Strategies::Base
|
32
|
-
delegate :valid?, to: :api_request
|
21
|
+
The usage is pretty simple. To sign a request use ApiSignature::Signer and for validation use ApiSignature::Validator.
|
33
22
|
|
34
|
-
|
35
|
-
# Find client in database by public api_key
|
36
|
-
resource = Client.find_for_token_authentication(api_request.access_key)
|
37
|
-
return fail!(:not_found_in_database) unless resource
|
23
|
+
### Create signature
|
38
24
|
|
39
|
-
|
40
|
-
return unless api_request.correct?(resource.api_key, resource.api_secret)
|
25
|
+
Sign a request with 'authorization' header. You can change header name, see Configuration section.
|
41
26
|
|
42
|
-
|
43
|
-
|
27
|
+
```ruby
|
28
|
+
api_access_key = 'access_key'
|
29
|
+
api_secret_key = 'secret_key'
|
30
|
+
|
31
|
+
request = {
|
32
|
+
http_method: 'POST',
|
33
|
+
url: 'https://example.com/posts',
|
34
|
+
headers: {
|
35
|
+
'User-Agent' => 'Test agent'
|
36
|
+
},
|
37
|
+
body: 'body'
|
38
|
+
}
|
44
39
|
|
45
|
-
|
46
|
-
|
47
|
-
end
|
40
|
+
# Sign your request
|
41
|
+
signature = ApiSignature::Signer.new(api_access_key, api_secret_key).sign_request(request)
|
48
42
|
|
49
|
-
|
43
|
+
# Now apply signed headers to your real request
|
44
|
+
signature.headers
|
50
45
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
46
|
+
# signature.headers looks like:
|
47
|
+
{
|
48
|
+
"host"=>"example.com",
|
49
|
+
"x-datetime"=>"2020-01-02T10:24:59.837+0000",
|
50
|
+
"authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
|
51
|
+
}
|
57
52
|
```
|
58
53
|
|
59
|
-
|
60
|
-
module MyApplication
|
61
|
-
module API
|
62
|
-
module Authentication
|
63
|
-
extend ActiveSupport::Concern
|
64
|
-
|
65
|
-
protected
|
66
|
-
|
67
|
-
def warden
|
68
|
-
@warden ||= request.env['warden']
|
69
|
-
end
|
70
|
-
|
71
|
-
def current_client
|
72
|
-
@current_client ||= warden.user(:client)
|
73
|
-
end
|
54
|
+
### Validate signature
|
74
55
|
|
75
|
-
|
76
|
-
warden.authenticate!(:client_authenticatable, scope: :client)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
```
|
56
|
+
Validate the request on the client-side. Note, that access_key can be extracted from the request.
|
82
57
|
|
83
58
|
```ruby
|
84
|
-
|
85
|
-
|
59
|
+
# the request to validate
|
60
|
+
request = {
|
61
|
+
:http_method=>"POST",
|
62
|
+
:url=>"https://example.com/posts",
|
63
|
+
:headers=>{
|
64
|
+
"User-Agent"=>"Test agent",
|
65
|
+
"host"=>"example.com",
|
66
|
+
"x-datetime"=>"2020-01-02T10:24:59.837+0000",
|
67
|
+
"authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
|
68
|
+
},
|
69
|
+
:body=>"body"
|
70
|
+
}
|
86
71
|
|
87
|
-
|
72
|
+
# initialize validator with a request to validate
|
73
|
+
validator = ApiSignature::Validator.new(request)
|
88
74
|
|
89
|
-
|
90
|
-
|
91
|
-
```
|
75
|
+
# get access key from request headers (String)
|
76
|
+
validator.access_key
|
92
77
|
|
93
|
-
|
78
|
+
# validate the request (Boolean)
|
79
|
+
validator.valid?('your secret key here')
|
94
80
|
|
95
|
-
|
96
|
-
|
97
|
-
request_method: 'GET',
|
98
|
-
path: '/api/v1/some_path'
|
99
|
-
access_key: 'client public api_key',
|
100
|
-
timestamp: Time.now.utc.to_i
|
101
|
-
}
|
102
|
-
|
103
|
-
signature = ApiSignature::Generator.new(options).generate_signature('api_secret')
|
81
|
+
# get only signed headers (Hash)
|
82
|
+
validator.signed_headers
|
104
83
|
```
|
105
84
|
|
106
|
-
|
85
|
+
## Configuration
|
86
|
+
|
87
|
+
By default, the generated signature will be valid for 5 minutes
|
107
88
|
This could be changed via initializer:
|
108
89
|
|
109
90
|
```ruby
|
110
91
|
# config/initializers/api_signature.rb
|
111
92
|
|
112
93
|
ApiSignature.setup do |config|
|
113
|
-
|
94
|
+
# Time to live, by default 5 minutes
|
95
|
+
config.signature_ttl = 5 * 60
|
96
|
+
|
97
|
+
# Datetime format, by default iso8601
|
98
|
+
config.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'
|
99
|
+
|
100
|
+
# Header name, by default authorization
|
101
|
+
config.signature_header = 'authorization'
|
102
|
+
|
103
|
+
# Service name, by default web
|
104
|
+
config.service = 'web'
|
114
105
|
end
|
115
106
|
```
|
116
107
|
|
data/api_signature.gemspec
CHANGED
@@ -21,12 +21,8 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
22
|
spec.require_paths = ['lib']
|
23
23
|
|
24
|
-
spec.add_development_dependency 'bundler', '~> 1.15'
|
25
24
|
spec.add_development_dependency 'guard-rspec', '~> 4.7', '>= 4.7.3'
|
26
25
|
spec.add_development_dependency 'rake', '~> 10.0'
|
27
26
|
spec.add_development_dependency 'rb-fsevent', '0.9.8'
|
28
27
|
spec.add_development_dependency 'rspec', '~> 3.0'
|
29
|
-
|
30
|
-
spec.add_dependency 'activesupport', '>= 4.0'
|
31
|
-
spec.add_dependency 'rack', '>= 1.6'
|
32
28
|
end
|
data/lib/api_signature.rb
CHANGED
@@ -1,19 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'api_signature/version'
|
4
|
-
require '
|
5
|
-
require 'active_support/core_ext/class'
|
6
|
-
require 'active_support/core_ext/object/try'
|
4
|
+
require 'api_signature/configuration'
|
7
5
|
|
8
6
|
module ApiSignature
|
9
7
|
autoload :Builder, 'api_signature/builder'
|
10
8
|
autoload :Validator, 'api_signature/validator'
|
11
|
-
autoload :
|
12
|
-
autoload :
|
9
|
+
autoload :Signer, 'api_signature/signer'
|
10
|
+
autoload :Signature, 'api_signature/signature'
|
11
|
+
autoload :AuthHeader, 'api_signature/auth_header'
|
12
|
+
autoload :Utils, 'api_signature/utils'
|
13
13
|
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
class << self
|
15
|
+
attr_writer :configuration
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.configuration
|
19
|
+
@configuration ||= Configuration.new
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.reset
|
23
|
+
@configuration = Configuration.new
|
24
|
+
end
|
17
25
|
|
18
26
|
# @example
|
19
27
|
# ApiSignature.setup do |config|
|
@@ -21,6 +29,6 @@ module ApiSignature
|
|
21
29
|
# end
|
22
30
|
#
|
23
31
|
def self.setup
|
24
|
-
yield
|
32
|
+
yield configuration
|
25
33
|
end
|
26
34
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSignature
|
4
|
+
class AuthHeader
|
5
|
+
attr_reader :authorization
|
6
|
+
|
7
|
+
TOKEN_REGEX = /^(API-HMAC-SHA256)\s+/.freeze
|
8
|
+
AUTHN_PAIR_DELIMITERS = /(?:,|\t+)/.freeze
|
9
|
+
|
10
|
+
def initialize(authorization)
|
11
|
+
@authorization = authorization.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
def credential
|
15
|
+
data[0]
|
16
|
+
end
|
17
|
+
|
18
|
+
def signature
|
19
|
+
options['Signature']
|
20
|
+
end
|
21
|
+
|
22
|
+
def signed_headers
|
23
|
+
return [] unless options['SignedHeaders']
|
24
|
+
|
25
|
+
@signed_headers ||= options['SignedHeaders'].split(/;\s?/).map(&:strip)
|
26
|
+
end
|
27
|
+
|
28
|
+
def options
|
29
|
+
@options ||= (data[1] || {})
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def data
|
35
|
+
@data ||= (parse_token_with_options || [])
|
36
|
+
end
|
37
|
+
|
38
|
+
def parse_token_with_options
|
39
|
+
return unless authorization[TOKEN_REGEX]
|
40
|
+
|
41
|
+
params = token_params_from authorization
|
42
|
+
[params.shift[1], Hash[params]]
|
43
|
+
end
|
44
|
+
|
45
|
+
def token_params_from(auth)
|
46
|
+
rewrite_param_values params_array_from raw_params(auth)
|
47
|
+
end
|
48
|
+
|
49
|
+
def raw_params(auth)
|
50
|
+
auth.sub(TOKEN_REGEX, '').split(/\s*#{AUTHN_PAIR_DELIMITERS}\s*/)
|
51
|
+
end
|
52
|
+
|
53
|
+
def params_array_from(raw_params)
|
54
|
+
raw_params.map { |param| param.split(/=(.+)?/) }
|
55
|
+
end
|
56
|
+
|
57
|
+
def rewrite_param_values(array_params)
|
58
|
+
array_params.each { |param| (param[1] || +'').gsub!(/^"|"$/, '') }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -1,51 +1,119 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require '
|
4
|
-
require 'ostruct'
|
3
|
+
require 'uri'
|
5
4
|
|
6
5
|
module ApiSignature
|
7
6
|
class Builder
|
8
|
-
|
9
|
-
:access_key, :secret, :request_method, :path, :timestamp
|
10
|
-
].freeze
|
7
|
+
attr_reader :settings
|
11
8
|
|
12
|
-
|
13
|
-
delegate :expired?, to: :signature_generator
|
9
|
+
SPLITTER = "\n"
|
14
10
|
|
15
|
-
def initialize(settings = {})
|
16
|
-
settings =
|
11
|
+
def initialize(settings = {}, unsigned_headers = [])
|
12
|
+
@settings = settings
|
13
|
+
@unsigned_headers = unsigned_headers
|
14
|
+
end
|
15
|
+
|
16
|
+
def http_method
|
17
|
+
@http_method ||= extract_http_method
|
18
|
+
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
+
def uri
|
21
|
+
@uri ||= extract_uri
|
22
|
+
end
|
20
23
|
|
21
|
-
|
24
|
+
def host
|
25
|
+
@host ||= extract_host_from_uri
|
22
26
|
end
|
23
27
|
|
24
28
|
def headers
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
29
|
+
@headers ||= Utils.normalize_keys(settings[:headers])
|
30
|
+
end
|
31
|
+
|
32
|
+
def datetime
|
33
|
+
@datetime ||= extract_datetime
|
34
|
+
end
|
35
|
+
|
36
|
+
def date
|
37
|
+
@date ||= datetime.to_s.scan(/\d/).take(8).join
|
38
|
+
end
|
39
|
+
|
40
|
+
def content_sha256
|
41
|
+
@content_sha256 ||= Utils.sha256_hexdigest(body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def body
|
45
|
+
@body ||= (settings[:body] || '')
|
30
46
|
end
|
31
47
|
|
32
|
-
def
|
33
|
-
{
|
34
|
-
|
35
|
-
|
36
|
-
path: path,
|
37
|
-
access_key: access_key
|
48
|
+
def build_sign_headers(apply_checksum_header = false)
|
49
|
+
@sign_headers = {
|
50
|
+
'host' => host,
|
51
|
+
'x-datetime' => datetime
|
38
52
|
}
|
53
|
+
@sign_headers['x-content-sha256'] = content_sha256 if apply_checksum_header
|
54
|
+
@sign_headers
|
55
|
+
end
|
56
|
+
|
57
|
+
def full_headers
|
58
|
+
@full_headers ||= merge_sign_with_origin_headers
|
59
|
+
end
|
60
|
+
|
61
|
+
def signed_headers
|
62
|
+
@signed_headers ||= full_headers.reject { |key, _value| @unsigned_headers.include?(key) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def signed_headers_names
|
66
|
+
@signed_headers_names ||= signed_headers.keys.sort.join(';')
|
39
67
|
end
|
40
68
|
|
41
|
-
def
|
42
|
-
|
69
|
+
def canonical_request(path)
|
70
|
+
[
|
71
|
+
http_method,
|
72
|
+
path,
|
73
|
+
Utils.normalized_querystring(uri.query),
|
74
|
+
canonical_headers + SPLITTER,
|
75
|
+
signed_headers_names,
|
76
|
+
content_sha256
|
77
|
+
].join(SPLITTER)
|
43
78
|
end
|
44
79
|
|
45
80
|
private
|
46
81
|
|
47
|
-
def
|
48
|
-
|
82
|
+
def extract_http_method
|
83
|
+
raise ArgumentError, 'missing required option :http_method' unless settings[:http_method]
|
84
|
+
|
85
|
+
settings[:http_method].to_s.upcase
|
86
|
+
end
|
87
|
+
|
88
|
+
def extract_uri
|
89
|
+
raise ArgumentError, 'missing required option :url' unless settings[:url]
|
90
|
+
|
91
|
+
URI.parse(settings[:url].to_s)
|
92
|
+
end
|
93
|
+
|
94
|
+
def extract_host_from_uri
|
95
|
+
if Utils.standard_port?(uri)
|
96
|
+
uri.host
|
97
|
+
else
|
98
|
+
"#{uri.host}:#{uri.port}"
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def extract_datetime
|
103
|
+
headers['x-datetime'] || Time.now.utc.strftime(ApiSignature.configuration.datetime_format)
|
104
|
+
end
|
105
|
+
|
106
|
+
def merge_sign_with_origin_headers
|
107
|
+
raise ArgumentError, 'missing required variable sign_headers' unless @sign_headers
|
108
|
+
|
109
|
+
# merge so we do not modify given headers hash
|
110
|
+
headers.merge(@sign_headers)
|
111
|
+
end
|
112
|
+
|
113
|
+
def canonical_headers
|
114
|
+
signed_headers.sort_by(&:first)
|
115
|
+
.map { |k, v| "#{k}:#{Utils.canonical_header_value(v.to_s)}" }
|
116
|
+
.join(SPLITTER)
|
49
117
|
end
|
50
118
|
end
|
51
119
|
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSignature
|
4
|
+
class Configuration
|
5
|
+
attr_accessor :signature_ttl, :signature_header, :datetime_format, :service
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@signature_ttl = 5 * 60
|
9
|
+
@datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'
|
10
|
+
@signature_header = 'authorization'
|
11
|
+
@service = 'web'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSignature
|
4
|
+
class Signature
|
5
|
+
# @return [Hash<String,String>] A hash of headers that should
|
6
|
+
# be applied to the HTTP request. Header keys are lower
|
7
|
+
# cased strings and may include the following:
|
8
|
+
#
|
9
|
+
# * 'host'
|
10
|
+
# * 'x-date'
|
11
|
+
# * 'x-content-sha256'
|
12
|
+
# * 'authorization'
|
13
|
+
#
|
14
|
+
attr_reader :headers
|
15
|
+
|
16
|
+
# @return [String] For debugging purposes.
|
17
|
+
attr_reader :canonical_request
|
18
|
+
|
19
|
+
# @return [String] For debugging purposes.
|
20
|
+
attr_reader :string_to_sign
|
21
|
+
|
22
|
+
# @return [String] For debugging purposes.
|
23
|
+
attr_reader :content_sha256
|
24
|
+
|
25
|
+
# @return [String] For debugging purposes.
|
26
|
+
attr_reader :signature
|
27
|
+
|
28
|
+
def initialize(attributes)
|
29
|
+
attributes.each do |key, value|
|
30
|
+
instance_variable_set("@#{key}", value)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ApiSignature
|
6
|
+
# The signer requires secret key.
|
7
|
+
#
|
8
|
+
# signer = ApiSignature::Signer.new('access key', 'secret key', uri_escape_path: true)
|
9
|
+
#
|
10
|
+
class Signer
|
11
|
+
NAME = 'API-HMAC-SHA256'
|
12
|
+
|
13
|
+
# Options:
|
14
|
+
# @option options [Array<String>] :unsigned_headers ([]) A list of
|
15
|
+
# headers that should not be signed. This is useful when a proxy
|
16
|
+
# modifies headers, such as 'User-Agent', invalidating a signature.
|
17
|
+
#
|
18
|
+
# @option options [Boolean] :uri_escape_path (true) When `true`,
|
19
|
+
# the request URI path is uri-escaped as part of computing the canonical
|
20
|
+
# request string.
|
21
|
+
#
|
22
|
+
# @option options [Boolean] :apply_checksum_header (false) When `true`,
|
23
|
+
# the computed content checksum is returned in the hash of signature
|
24
|
+
# headers.
|
25
|
+
#
|
26
|
+
# @option options [String] :signature_header (authorization) Header name
|
27
|
+
# for signature
|
28
|
+
#
|
29
|
+
# @option options [String] :service (web) Service name
|
30
|
+
#
|
31
|
+
def initialize(access_key, secret_key, options = {})
|
32
|
+
@access_key = access_key
|
33
|
+
@secret_key = secret_key
|
34
|
+
@options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
# Computes a signature. Returns the resultant
|
38
|
+
# signature as a hash of headers to apply to your HTTP request. The given
|
39
|
+
# request is not modified.
|
40
|
+
#
|
41
|
+
# signature = signer.sign_request(
|
42
|
+
# http_method: 'PUT',
|
43
|
+
# url: 'https://domain.com',
|
44
|
+
# headers: {
|
45
|
+
# 'Abc' => 'xyz',
|
46
|
+
# },
|
47
|
+
# body: 'body' # String or IO object
|
48
|
+
# )
|
49
|
+
# @param [Hash] request
|
50
|
+
#
|
51
|
+
# @option request [required, String] :http_method One of
|
52
|
+
# 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'
|
53
|
+
#
|
54
|
+
# @option request [required, String, URI::HTTPS, URI::HTTP] :url
|
55
|
+
# The request URI. Must be a valid HTTP or HTTPS URI.
|
56
|
+
#
|
57
|
+
# @option request [optional, Hash] :headers ({}) A hash of headers
|
58
|
+
# to sign. If the 'X-Amz-Content-Sha256' header is set, the `:body`
|
59
|
+
# is optional and will not be read.
|
60
|
+
#
|
61
|
+
# @option request [optional, String, IO] :body ('') The HTTP request body.
|
62
|
+
# A sha256 checksum is computed of the body unless the
|
63
|
+
# 'X-Amz-Content-Sha256' header is set.
|
64
|
+
#
|
65
|
+
# @return [Signature] Return an instance of {Signature} that has
|
66
|
+
# a `#headers` method. The headers must be applied to your request.
|
67
|
+
def sign_request(request)
|
68
|
+
builder = Builder.new(request, unsigned_headers)
|
69
|
+
sig_headers = builder.build_sign_headers(apply_checksum_header?)
|
70
|
+
data = build_signature(builder)
|
71
|
+
|
72
|
+
# apply signature
|
73
|
+
sig_headers[signature_header_name] = data[:header]
|
74
|
+
|
75
|
+
# Returning the signature components.
|
76
|
+
Signature.new(data.merge!(headers: sig_headers))
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def uri_escape_path?
|
82
|
+
@options[:uri_escape_path] == true || !@options.key?(:uri_escape_path)
|
83
|
+
end
|
84
|
+
|
85
|
+
def apply_checksum_header?
|
86
|
+
@options[:apply_checksum_header] == true
|
87
|
+
end
|
88
|
+
|
89
|
+
def signature_header_name
|
90
|
+
@options[:signature_header] || ApiSignature.configuration.signature_header
|
91
|
+
end
|
92
|
+
|
93
|
+
def service
|
94
|
+
@options[:service] || ApiSignature.configuration.service
|
95
|
+
end
|
96
|
+
|
97
|
+
def unsigned_headers
|
98
|
+
@unsigned_headers ||= build_unsigned_headers
|
99
|
+
end
|
100
|
+
|
101
|
+
def build_unsigned_headers
|
102
|
+
Set.new(@options.fetch(:unsigned_headers, []).map(&:downcase)) << signature_header_name
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_signature(builder)
|
106
|
+
path = Utils.url_path(builder.uri.path, uri_escape_path?)
|
107
|
+
|
108
|
+
# compute signature parts
|
109
|
+
creq = builder.canonical_request(path)
|
110
|
+
sts = string_to_sign(builder.datetime, creq)
|
111
|
+
sig = signature(builder.date, sts)
|
112
|
+
|
113
|
+
{
|
114
|
+
header: build_signature_header(builder, sig),
|
115
|
+
content_sha256: builder.content_sha256,
|
116
|
+
string_to_sign: sts,
|
117
|
+
canonical_request: creq,
|
118
|
+
signature: sig
|
119
|
+
}
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_signature_header(builder, signature)
|
123
|
+
[
|
124
|
+
"#{NAME} Credential=#{credential(builder.date)}",
|
125
|
+
"SignedHeaders=#{builder.signed_headers_names}",
|
126
|
+
"Signature=#{signature}"
|
127
|
+
].join(', ')
|
128
|
+
end
|
129
|
+
|
130
|
+
def string_to_sign(datetime, canonical_request)
|
131
|
+
[
|
132
|
+
NAME,
|
133
|
+
datetime,
|
134
|
+
Utils.sha256_hexdigest(canonical_request)
|
135
|
+
].join(Builder::SPLITTER)
|
136
|
+
end
|
137
|
+
|
138
|
+
def signature(date, string_to_sign)
|
139
|
+
k_date = Utils.hmac("API#{@secret_key}", date)
|
140
|
+
k_service = Utils.hmac(k_date, service)
|
141
|
+
k_credentials = Utils.hmac(k_service, 'api_request')
|
142
|
+
|
143
|
+
Utils.hexhmac(k_credentials, string_to_sign)
|
144
|
+
end
|
145
|
+
|
146
|
+
def credential(date)
|
147
|
+
"#{@access_key}/#{date}/#{service}/api_request"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -33,11 +33,16 @@ module ApiSignature
|
|
33
33
|
private
|
34
34
|
|
35
35
|
def with_signature(http_method, api_key, secret, action_name, params = {})
|
36
|
+
custom_headers = (params.delete(:headers) || {})
|
36
37
|
path = PathBuilder.new(controller, action_name, params).path
|
37
|
-
headers = HeadersBuilder.new(api_key, secret, http_method, path).headers
|
38
|
-
custom_headers = params.delete(:headers) || {}
|
39
38
|
|
40
|
-
|
39
|
+
signature = Signer.new(api_key, secret).sign_request(
|
40
|
+
http_method: http_method.to_s.upcase,
|
41
|
+
url: path,
|
42
|
+
headers: custom_headers
|
43
|
+
)
|
44
|
+
|
45
|
+
send(http_method, path, params, signature.headers)
|
41
46
|
end
|
42
47
|
end
|
43
48
|
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'openssl'
|
4
|
+
require 'digest/sha1'
|
5
|
+
require 'tempfile'
|
6
|
+
require 'date'
|
7
|
+
|
8
|
+
module ApiSignature
|
9
|
+
module Utils
|
10
|
+
# @param [File, Tempfile, IO#read, String] value
|
11
|
+
# @return [String<SHA256 Hexdigest>]
|
12
|
+
#
|
13
|
+
def self.sha256_hexdigest(value)
|
14
|
+
if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
|
15
|
+
OpenSSL::Digest::SHA256.file(value).hexdigest
|
16
|
+
elsif value.respond_to?(:read)
|
17
|
+
sha256 = OpenSSL::Digest::SHA256.new
|
18
|
+
|
19
|
+
while chunk = value.read(1024 * 1024, buffer ||= '') # 1MB
|
20
|
+
sha256.update(chunk)
|
21
|
+
end
|
22
|
+
|
23
|
+
value.rewind
|
24
|
+
sha256.hexdigest
|
25
|
+
else
|
26
|
+
OpenSSL::Digest::SHA256.hexdigest(value)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @param [URI] uri
|
31
|
+
# @return [true/false]
|
32
|
+
#
|
33
|
+
def self.standard_port?(uri)
|
34
|
+
(uri.scheme == 'http' && uri.port == 80) ||
|
35
|
+
(uri.scheme == 'https' && uri.port == 443)
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.url_path(path, uri_escape_path = false)
|
39
|
+
path = '/' if path == ''
|
40
|
+
|
41
|
+
if uri_escape_path
|
42
|
+
uri_escape_path(path)
|
43
|
+
else
|
44
|
+
path
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.uri_escape_path(path)
|
49
|
+
path.gsub(/[^\/]+/) { |part| uri_escape(part) }
|
50
|
+
end
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def self.uri_escape(string)
|
54
|
+
if string.nil?
|
55
|
+
nil
|
56
|
+
else
|
57
|
+
CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.normalized_querystring(querystring)
|
62
|
+
return unless querystring
|
63
|
+
|
64
|
+
params = querystring.split('&')
|
65
|
+
params = params.map { |p| p.match(/=/) ? p : p + '=' }
|
66
|
+
# We have to sort by param name and preserve order of params that
|
67
|
+
# have the same name. Default sort <=> in JRuby will swap members
|
68
|
+
# occasionally when <=> is 0 (considered still sorted), but this
|
69
|
+
# causes our normalized query string to not match the sent querystring.
|
70
|
+
# When names match, we then sort by their original order
|
71
|
+
params.each.with_index.sort do |a, b|
|
72
|
+
a, a_offset = a
|
73
|
+
a_name = a.split('=')[0]
|
74
|
+
b, b_offset = b
|
75
|
+
b_name = b.split('=')[0]
|
76
|
+
if a_name == b_name
|
77
|
+
a_offset <=> b_offset
|
78
|
+
else
|
79
|
+
a_name <=> b_name
|
80
|
+
end
|
81
|
+
end.map(&:first).join('&')
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.canonical_header_value(value)
|
85
|
+
value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
|
86
|
+
end
|
87
|
+
|
88
|
+
def self.hmac(key, value)
|
89
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.hexhmac(key, value)
|
93
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.normalize_keys(hash)
|
97
|
+
return {} unless hash
|
98
|
+
|
99
|
+
hash.transform_keys { |key| key.to_s.downcase }
|
100
|
+
end
|
101
|
+
|
102
|
+
# constant-time comparison algorithm to prevent timing attacks
|
103
|
+
def self.secure_compare(string_a, string_b)
|
104
|
+
return false if string_a.nil? || string_b.nil? || string_a.bytesize != string_b.bytesize
|
105
|
+
|
106
|
+
l = string_a.unpack "C#{string_a.bytesize}"
|
107
|
+
|
108
|
+
res = 0
|
109
|
+
string_b.each_byte { |byte| res |= byte ^ l.shift }
|
110
|
+
res == 0
|
111
|
+
end
|
112
|
+
|
113
|
+
def self.safe_parse_datetime(value, format = nil)
|
114
|
+
format ||= ApiSignature.configuration.datetime_format
|
115
|
+
DateTime.strptime(value, format)
|
116
|
+
rescue ArgumentError => _e
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -1,39 +1,98 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ApiSignature
|
4
|
+
# Validate a request
|
5
|
+
#
|
6
|
+
# request = {
|
7
|
+
# http_method: 'PUT',
|
8
|
+
# url: 'https://domain.com',
|
9
|
+
# headers: {
|
10
|
+
# 'Authorization' => 'API-HMAC-SHA256 Credential=access_key/20191227/api_request...',
|
11
|
+
# 'Host' => 'example.com,
|
12
|
+
# 'X-Content-Sha256' => '...',
|
13
|
+
# 'X-Datetime' => '2019-12-27T09:13:14.873+0000'
|
14
|
+
# },
|
15
|
+
# body: 'body'
|
16
|
+
# }
|
17
|
+
# validator = ApiSignature::Validator.new(request, uri_escape_path: true)
|
18
|
+
# validator.access_key # get key from request headers
|
19
|
+
# validator.valid?('secret_key')
|
20
|
+
#
|
4
21
|
class Validator
|
5
|
-
attr_reader :
|
22
|
+
attr_reader :request
|
6
23
|
|
7
|
-
def initialize(options)
|
24
|
+
def initialize(request, options = {})
|
25
|
+
@request = request
|
8
26
|
@options = options
|
9
|
-
@timestamp = Time.at(@options[:timestamp].to_i).utc
|
10
27
|
end
|
11
28
|
|
12
|
-
def
|
13
|
-
return
|
14
|
-
|
29
|
+
def access_key
|
30
|
+
return unless valid_credential?
|
31
|
+
|
32
|
+
@access_key ||= auth_header.credential.split('/')[0]
|
33
|
+
end
|
34
|
+
|
35
|
+
def signed_headers
|
36
|
+
@signed_headers ||= headers.slice(*auth_header.signed_headers)
|
37
|
+
end
|
38
|
+
|
39
|
+
# Validate a signature. Returns boolean
|
40
|
+
#
|
41
|
+
# validator.valid?('secret_key_here')
|
42
|
+
#
|
43
|
+
# @param [String] secret key
|
44
|
+
#
|
45
|
+
def valid?(secret_key)
|
46
|
+
valid_authorization? && valid_timestamp? && valid_signature?(secret_key)
|
47
|
+
end
|
48
|
+
|
49
|
+
def valid_authorization?
|
50
|
+
valid_credential? && !auth_header.signature.nil?
|
51
|
+
end
|
52
|
+
|
53
|
+
def valid_credential?
|
54
|
+
!auth_header.credential.nil?
|
55
|
+
end
|
56
|
+
|
57
|
+
def valid_timestamp?
|
58
|
+
timestamp && ttl_range.cover?(timestamp.to_time)
|
15
59
|
end
|
16
60
|
|
17
|
-
def
|
18
|
-
|
61
|
+
def valid_signature?(secret_key)
|
62
|
+
return false unless secret_key
|
63
|
+
|
64
|
+
signer = Signer.new(access_key, secret_key, @options)
|
65
|
+
data = signer.sign_request(request)
|
66
|
+
|
67
|
+
Utils.secure_compare(
|
68
|
+
auth_header.signature,
|
69
|
+
data.signature
|
70
|
+
)
|
19
71
|
end
|
20
72
|
|
21
73
|
private
|
22
74
|
|
23
|
-
def
|
24
|
-
@
|
75
|
+
def auth_header
|
76
|
+
@auth_header ||= AuthHeader.new(headers[signature_header_name])
|
77
|
+
end
|
78
|
+
|
79
|
+
def signature_header_name
|
80
|
+
@options[:signature_header] || ApiSignature.configuration.signature_header
|
25
81
|
end
|
26
82
|
|
27
|
-
def
|
28
|
-
|
83
|
+
def timestamp
|
84
|
+
@timestamp ||= Utils.safe_parse_datetime(headers['x-datetime'])
|
29
85
|
end
|
30
86
|
|
31
|
-
def
|
32
|
-
@
|
87
|
+
def headers
|
88
|
+
@headers ||= Utils.normalize_keys(request[:headers])
|
33
89
|
end
|
34
90
|
|
35
|
-
def
|
36
|
-
|
91
|
+
def ttl_range
|
92
|
+
to = Time.now.utc
|
93
|
+
from = to - ApiSignature.configuration.signature_ttl
|
94
|
+
|
95
|
+
from..to
|
37
96
|
end
|
38
97
|
end
|
39
98
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: api_signature
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Igor Galeta
|
@@ -9,22 +9,8 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-01-07 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
|
-
- !ruby/object:Gem::Dependency
|
15
|
-
name: bundler
|
16
|
-
requirement: !ruby/object:Gem::Requirement
|
17
|
-
requirements:
|
18
|
-
- - "~>"
|
19
|
-
- !ruby/object:Gem::Version
|
20
|
-
version: '1.15'
|
21
|
-
type: :development
|
22
|
-
prerelease: false
|
23
|
-
version_requirements: !ruby/object:Gem::Requirement
|
24
|
-
requirements:
|
25
|
-
- - "~>"
|
26
|
-
- !ruby/object:Gem::Version
|
27
|
-
version: '1.15'
|
28
14
|
- !ruby/object:Gem::Dependency
|
29
15
|
name: guard-rspec
|
30
16
|
requirement: !ruby/object:Gem::Requirement
|
@@ -87,34 +73,6 @@ dependencies:
|
|
87
73
|
- - "~>"
|
88
74
|
- !ruby/object:Gem::Version
|
89
75
|
version: '3.0'
|
90
|
-
- !ruby/object:Gem::Dependency
|
91
|
-
name: activesupport
|
92
|
-
requirement: !ruby/object:Gem::Requirement
|
93
|
-
requirements:
|
94
|
-
- - ">="
|
95
|
-
- !ruby/object:Gem::Version
|
96
|
-
version: '4.0'
|
97
|
-
type: :runtime
|
98
|
-
prerelease: false
|
99
|
-
version_requirements: !ruby/object:Gem::Requirement
|
100
|
-
requirements:
|
101
|
-
- - ">="
|
102
|
-
- !ruby/object:Gem::Version
|
103
|
-
version: '4.0'
|
104
|
-
- !ruby/object:Gem::Dependency
|
105
|
-
name: rack
|
106
|
-
requirement: !ruby/object:Gem::Requirement
|
107
|
-
requirements:
|
108
|
-
- - ">="
|
109
|
-
- !ruby/object:Gem::Version
|
110
|
-
version: '1.6'
|
111
|
-
type: :runtime
|
112
|
-
prerelease: false
|
113
|
-
version_requirements: !ruby/object:Gem::Requirement
|
114
|
-
requirements:
|
115
|
-
- - ">="
|
116
|
-
- !ruby/object:Gem::Version
|
117
|
-
version: '1.6'
|
118
76
|
description:
|
119
77
|
email:
|
120
78
|
- igor.malinovskiy@netfix.xyz
|
@@ -125,6 +83,7 @@ files:
|
|
125
83
|
- ".gitignore"
|
126
84
|
- ".rspec"
|
127
85
|
- ".rubocop.yml"
|
86
|
+
- ".ruby-version"
|
128
87
|
- ".travis.yml"
|
129
88
|
- Gemfile
|
130
89
|
- Guardfile
|
@@ -135,12 +94,14 @@ files:
|
|
135
94
|
- bin/console
|
136
95
|
- bin/setup
|
137
96
|
- lib/api_signature.rb
|
97
|
+
- lib/api_signature/auth_header.rb
|
138
98
|
- lib/api_signature/builder.rb
|
139
|
-
- lib/api_signature/
|
140
|
-
- lib/api_signature/
|
141
|
-
- lib/api_signature/
|
99
|
+
- lib/api_signature/configuration.rb
|
100
|
+
- lib/api_signature/signature.rb
|
101
|
+
- lib/api_signature/signer.rb
|
142
102
|
- lib/api_signature/spec_support/helper.rb
|
143
103
|
- lib/api_signature/spec_support/path_builder.rb
|
104
|
+
- lib/api_signature/utils.rb
|
144
105
|
- lib/api_signature/validator.rb
|
145
106
|
- lib/api_signature/version.rb
|
146
107
|
homepage: https://github.com/psyipm/api_signature
|
@@ -163,7 +124,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
163
124
|
version: '0'
|
164
125
|
requirements: []
|
165
126
|
rubyforge_project:
|
166
|
-
rubygems_version: 2.6.
|
127
|
+
rubygems_version: 2.7.6.2
|
167
128
|
signing_key:
|
168
129
|
specification_version: 4
|
169
130
|
summary: Sign API requests with HMAC signature
|
@@ -1,42 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'openssl'
|
4
|
-
require 'digest/sha1'
|
5
|
-
|
6
|
-
module ApiSignature
|
7
|
-
class Generator
|
8
|
-
SPLITTER = '|'
|
9
|
-
|
10
|
-
delegate :valid?, :expired?, :timestamp, to: :validator
|
11
|
-
|
12
|
-
attr_reader :options
|
13
|
-
|
14
|
-
def initialize(options = {})
|
15
|
-
@options = options
|
16
|
-
end
|
17
|
-
|
18
|
-
def generate_signature(secret)
|
19
|
-
hmac = OpenSSL::HMAC.digest(digest, secret, string_to_sign)
|
20
|
-
Base64.encode64(hmac).chomp
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def validator
|
26
|
-
Validator.new(options)
|
27
|
-
end
|
28
|
-
|
29
|
-
def digest
|
30
|
-
OpenSSL::Digest::SHA256.new
|
31
|
-
end
|
32
|
-
|
33
|
-
def string_to_sign
|
34
|
-
[
|
35
|
-
options[:request_method],
|
36
|
-
options[:path],
|
37
|
-
options[:access_key],
|
38
|
-
timestamp.to_i
|
39
|
-
].map(&:to_s).join(SPLITTER)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
@@ -1,48 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'rack/request'
|
4
|
-
|
5
|
-
module ApiSignature
|
6
|
-
class Request < ::Rack::Request
|
7
|
-
HEADER_KEYS = {
|
8
|
-
access_key: 'HTTP_X_ACCESS_KEY',
|
9
|
-
signature: 'HTTP_X_SIGNATURE',
|
10
|
-
timestamp: 'HTTP_X_TIMESTAMP'
|
11
|
-
}.freeze
|
12
|
-
|
13
|
-
def correct?(token, secret)
|
14
|
-
access_key == token && validator.valid?(signature, secret)
|
15
|
-
end
|
16
|
-
|
17
|
-
def valid?
|
18
|
-
timestamp.present? && signature.present? && access_key.present?
|
19
|
-
end
|
20
|
-
|
21
|
-
def timestamp
|
22
|
-
@timestamp ||= @env[HEADER_KEYS[:timestamp]]
|
23
|
-
end
|
24
|
-
|
25
|
-
def signature
|
26
|
-
@signature ||= @env[HEADER_KEYS[:signature]]
|
27
|
-
end
|
28
|
-
|
29
|
-
def access_key
|
30
|
-
@access_key ||= @env[HEADER_KEYS[:access_key]]
|
31
|
-
end
|
32
|
-
|
33
|
-
private
|
34
|
-
|
35
|
-
def validator
|
36
|
-
@validator ||= Validator.new(validator_params)
|
37
|
-
end
|
38
|
-
|
39
|
-
def validator_params
|
40
|
-
{
|
41
|
-
request_method: request_method,
|
42
|
-
path: path,
|
43
|
-
access_key: access_key,
|
44
|
-
timestamp: timestamp
|
45
|
-
}
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
@@ -1,39 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ApiSignature
|
4
|
-
module SpecSupport
|
5
|
-
class HeadersBuilder
|
6
|
-
attr_reader :access_key, :secret, :http_method, :path
|
7
|
-
|
8
|
-
def initialize(access_key, secret, http_method, path)
|
9
|
-
@access_key = access_key
|
10
|
-
@secret = secret
|
11
|
-
@http_method = http_method
|
12
|
-
@path = path
|
13
|
-
end
|
14
|
-
|
15
|
-
def headers
|
16
|
-
{
|
17
|
-
'HTTP_X_ACCESS_KEY' => access_key,
|
18
|
-
'HTTP_X_TIMESTAMP' => options[:timestamp],
|
19
|
-
'HTTP_X_SIGNATURE' => generator.generate_signature(secret)
|
20
|
-
}
|
21
|
-
end
|
22
|
-
|
23
|
-
private
|
24
|
-
|
25
|
-
def generator
|
26
|
-
@generator ||= ::ApiSignature::Generator.new(options)
|
27
|
-
end
|
28
|
-
|
29
|
-
def options
|
30
|
-
@options ||= {
|
31
|
-
request_method: http_method.to_s.upcase,
|
32
|
-
path: path,
|
33
|
-
access_key: access_key,
|
34
|
-
timestamp: Time.now.utc.to_i
|
35
|
-
}
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|