galetahub-api-sigv2 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 +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +34 -0
- data/.ruby-version +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Guardfile +70 -0
- data/LICENSE.txt +21 -0
- data/README.md +177 -0
- data/Rakefile +6 -0
- data/api_sigv2.gemspec +28 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/api_sigv2.rb +34 -0
- data/lib/api_sigv2/auth_header.rb +61 -0
- data/lib/api_sigv2/builder.rb +119 -0
- data/lib/api_sigv2/configuration.rb +14 -0
- data/lib/api_sigv2/signature.rb +34 -0
- data/lib/api_sigv2/signer.rb +150 -0
- data/lib/api_sigv2/spec_support/helper.rb +49 -0
- data/lib/api_sigv2/spec_support/path_builder.rb +42 -0
- data/lib/api_sigv2/utils.rb +120 -0
- data/lib/api_sigv2/validator.rb +98 -0
- data/lib/api_sigv2/version.rb +5 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 52becac90c26c1d17bd80aaf90908b1fc2e0b04f5c8b47cc51d2a8ce519f7c98
|
4
|
+
data.tar.gz: 8b8d28cc1f3c97d2e5a0dbc093b48563f7a03c99ad8477a768333fb5a8fc3996
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 32085ab3aa761531c2e85676a4346a63eddbd33b1fdfc05300dbb06cc79c671eb61b03c5e58a38f48791c524096240d5696d5046bbef46d491e2881a60d6ebb0
|
7
|
+
data.tar.gz: abf526249bab793a86bfd26203a24d4f389d4a12b57655512652860ab8ced50bc50155f0c436261fb5f4d726614d1e39e851919cbbac9e29832f74b086e3bec3
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
AllCops:
|
2
|
+
TargetRubyVersion: 2.5.5
|
3
|
+
Exclude:
|
4
|
+
- "bin/*"
|
5
|
+
- "Guardfile"
|
6
|
+
|
7
|
+
##################### Styles ##################################
|
8
|
+
|
9
|
+
Style/Documentation:
|
10
|
+
Enabled: false
|
11
|
+
|
12
|
+
Style/SymbolArray:
|
13
|
+
Enabled: false
|
14
|
+
|
15
|
+
##################### Metrics ##################################
|
16
|
+
|
17
|
+
Metrics/LineLength:
|
18
|
+
Max: 110
|
19
|
+
|
20
|
+
Metrics/ClassLength:
|
21
|
+
Max: 200
|
22
|
+
|
23
|
+
Metrics/ModuleLength:
|
24
|
+
Max: 200
|
25
|
+
Exclude:
|
26
|
+
- "**/*_spec.rb"
|
27
|
+
|
28
|
+
Metrics/BlockLength:
|
29
|
+
Max: 50
|
30
|
+
Exclude:
|
31
|
+
- "**/*_spec.rb"
|
32
|
+
|
33
|
+
Metrics/MethodLength:
|
34
|
+
Max: 15
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
2.5.5
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
## Uncomment and set this to only include directories you want to watch
|
5
|
+
# directories %w(app lib config test spec features) \
|
6
|
+
# .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")}
|
7
|
+
|
8
|
+
## Note: if you are using the `directories` clause above and you are not
|
9
|
+
## watching the project directory ('.'), then you will want to move
|
10
|
+
## the Guardfile to a watched dir and symlink it back, e.g.
|
11
|
+
#
|
12
|
+
# $ mkdir config
|
13
|
+
# $ mv Guardfile config/
|
14
|
+
# $ ln -s config/Guardfile .
|
15
|
+
#
|
16
|
+
# and, you'll have to watch "config/Guardfile" instead of "Guardfile"
|
17
|
+
|
18
|
+
# Note: The cmd option is now required due to the increasing number of ways
|
19
|
+
# rspec may be run, below are examples of the most common uses.
|
20
|
+
# * bundler: 'bundle exec rspec'
|
21
|
+
# * bundler binstubs: 'bin/rspec'
|
22
|
+
# * spring: 'bin/rspec' (This will use spring if running and you have
|
23
|
+
# installed the spring binstubs per the docs)
|
24
|
+
# * zeus: 'zeus rspec' (requires the server to be started separately)
|
25
|
+
# * 'just' rspec: 'rspec'
|
26
|
+
|
27
|
+
guard :rspec, cmd: "bundle exec rspec" do
|
28
|
+
require "guard/rspec/dsl"
|
29
|
+
dsl = Guard::RSpec::Dsl.new(self)
|
30
|
+
|
31
|
+
# Feel free to open issues for suggestions and improvements
|
32
|
+
|
33
|
+
# RSpec files
|
34
|
+
rspec = dsl.rspec
|
35
|
+
watch(rspec.spec_helper) { rspec.spec_dir }
|
36
|
+
watch(rspec.spec_support) { rspec.spec_dir }
|
37
|
+
watch(rspec.spec_files)
|
38
|
+
|
39
|
+
# Ruby files
|
40
|
+
ruby = dsl.ruby
|
41
|
+
dsl.watch_spec_files_for(ruby.lib_files)
|
42
|
+
|
43
|
+
# Rails files
|
44
|
+
rails = dsl.rails(view_extensions: %w(erb haml slim))
|
45
|
+
dsl.watch_spec_files_for(rails.app_files)
|
46
|
+
dsl.watch_spec_files_for(rails.views)
|
47
|
+
|
48
|
+
watch(rails.controllers) do |m|
|
49
|
+
[
|
50
|
+
rspec.spec.call("routing/#{m[1]}_routing"),
|
51
|
+
rspec.spec.call("controllers/#{m[1]}_controller"),
|
52
|
+
rspec.spec.call("acceptance/#{m[1]}")
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Rails config changes
|
57
|
+
watch(rails.spec_helper) { rspec.spec_dir }
|
58
|
+
watch(rails.routes) { "#{rspec.spec_dir}/routing" }
|
59
|
+
watch(rails.app_controller) { "#{rspec.spec_dir}/controllers" }
|
60
|
+
|
61
|
+
# Capybara features specs
|
62
|
+
watch(rails.view_dirs) { |m| rspec.spec.call("features/#{m[1]}") }
|
63
|
+
watch(rails.layouts) { |m| rspec.spec.call("features/#{m[1]}") }
|
64
|
+
|
65
|
+
# Turnip features and steps
|
66
|
+
watch(%r{^spec/acceptance/(.+)\.feature$})
|
67
|
+
watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) do |m|
|
68
|
+
Dir[File.join("**/#{m[1]}.feature")][0] || "spec/acceptance"
|
69
|
+
end
|
70
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Igor Malinovskiy
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
# ApiSigv2
|
2
|
+
|
3
|
+
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)
|
4
|
+
|
5
|
+
This gem will generate signature for the client requests and verify that signature on the server side
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
gem 'api_sigv2'
|
13
|
+
```
|
14
|
+
|
15
|
+
## Usage
|
16
|
+
|
17
|
+
The usage is pretty simple. To sign a request use ApiSigv2::Signer and for validation use ApiSigv2::Validator.
|
18
|
+
|
19
|
+
### Create signature
|
20
|
+
|
21
|
+
Sign a request with 'authorization' header. You can change header name, see Configuration section.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
api_access_key = 'access_key'
|
25
|
+
api_secret_key = 'secret_key'
|
26
|
+
|
27
|
+
request = {
|
28
|
+
http_method: 'POST',
|
29
|
+
url: 'https://example.com/posts',
|
30
|
+
headers: {
|
31
|
+
'User-Agent' => 'Test agent'
|
32
|
+
},
|
33
|
+
body: 'body'
|
34
|
+
}
|
35
|
+
|
36
|
+
# Sign your request
|
37
|
+
signature = ApiSigv2::Signer.new(api_access_key, api_secret_key).sign_request(request)
|
38
|
+
|
39
|
+
# Now apply signed headers to your real request
|
40
|
+
signature.headers
|
41
|
+
|
42
|
+
# signature.headers looks like:
|
43
|
+
{
|
44
|
+
"host"=>"example.com",
|
45
|
+
"x-datetime"=>"2020-01-02T10:24:59.837+0000",
|
46
|
+
"authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
|
47
|
+
}
|
48
|
+
```
|
49
|
+
|
50
|
+
### Validate signature
|
51
|
+
|
52
|
+
Validate the request on the client-side. Note, that access_key can be extracted from the request.
|
53
|
+
|
54
|
+
```ruby
|
55
|
+
# the request to validate
|
56
|
+
request = {
|
57
|
+
:http_method=>"POST",
|
58
|
+
:url=>"https://example.com/posts",
|
59
|
+
:headers=>{
|
60
|
+
"User-Agent"=>"Test agent",
|
61
|
+
"host"=>"example.com",
|
62
|
+
"x-datetime"=>"2020-01-02T10:24:59.837+0000",
|
63
|
+
"authorization"=>"API-HMAC-SHA256 Credential=access_key/20200102/web/api_request, SignedHeaders=host;user-agent;x-datetime, Signature=032fc0b7defd66d86ef43ced8e6c3ee351ede21deca6bf1f89b9145f7a9105c1"
|
64
|
+
},
|
65
|
+
:body=>"body"
|
66
|
+
}
|
67
|
+
|
68
|
+
# initialize validator with a request to validate
|
69
|
+
validator = ApiSigv2::Validator.new(request)
|
70
|
+
|
71
|
+
# get access key from request headers (String)
|
72
|
+
validator.access_key
|
73
|
+
|
74
|
+
# validate the request (Boolean)
|
75
|
+
validator.valid?('your secret key here')
|
76
|
+
|
77
|
+
# get only signed headers (Hash)
|
78
|
+
validator.signed_headers
|
79
|
+
```
|
80
|
+
|
81
|
+
## Configuration
|
82
|
+
|
83
|
+
By default, the generated signature will be valid for 5 minutes
|
84
|
+
This could be changed via initializer:
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
# config/initializers/api_sigv2.rb
|
88
|
+
|
89
|
+
ApiSigv2.setup do |config|
|
90
|
+
# Time to live, by default 5 minutes
|
91
|
+
config.signature_ttl = 5 * 60
|
92
|
+
|
93
|
+
# Datetime format, by default iso8601
|
94
|
+
config.datetime_format = '%Y-%m-%dT%H:%M:%S.%L%z'
|
95
|
+
|
96
|
+
# Header name, by default authorization
|
97
|
+
config.signature_header = 'authorization'
|
98
|
+
|
99
|
+
# Service name, by default web
|
100
|
+
config.service = 'web'
|
101
|
+
end
|
102
|
+
```
|
103
|
+
|
104
|
+
## Testing
|
105
|
+
|
106
|
+
In your `rails_helper.rb`:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
require 'api_sigv2/spec_support/helper'
|
110
|
+
|
111
|
+
RSpec.configure do |config|
|
112
|
+
config.include ApiSigv2::SpecSupport::Helper, type: :controller
|
113
|
+
end
|
114
|
+
```
|
115
|
+
|
116
|
+
This will enable the following methods in controller tests:
|
117
|
+
|
118
|
+
* get_with_signature(client, action_name, params = {})
|
119
|
+
* post_with_signature(client, action_name, params = {})
|
120
|
+
* put_with_signature(client, action_name, params = {})
|
121
|
+
* patch_with_signature(client, action_name, params = {})
|
122
|
+
* delete_with_signature(client, action_name, params = {})
|
123
|
+
|
124
|
+
`client` object should respond to `#api_key` and `#api_secret`
|
125
|
+
|
126
|
+
Example usage:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
RSpec.describe Api::V1::OrdersController do
|
130
|
+
let(:client) { FactoryBot.create(:client) }
|
131
|
+
# or any object, that responds to #api_key and #api_secret
|
132
|
+
# let(:client) { OpenStruct.new(api_key: 'some_key', api_secret: 'some_api_secret') }
|
133
|
+
|
134
|
+
it 'should filter orders by state' do
|
135
|
+
get_with_signature client, :index, state: :paid
|
136
|
+
|
137
|
+
expect(last_response.status).to eq 200
|
138
|
+
expect(last_response.body).to have_node(:orders)
|
139
|
+
expect(last_response.body).to have_node(:state).with('paid')
|
140
|
+
end
|
141
|
+
|
142
|
+
let(:order_attributes) { FactoryBot.attributes_for(:order) }
|
143
|
+
|
144
|
+
it 'should create new order' do
|
145
|
+
post_with_signature client, :create, order: order_attributes
|
146
|
+
end
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
150
|
+
For nested resources path can be specified explicitly using `path` parameter:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
# path: /api/v1/orders/:order_id/comments
|
154
|
+
|
155
|
+
RSpec.describe Api::V1::CommentsController do
|
156
|
+
let(:client) { OpenStruct.new(api_key: 'some_key', api_secret: 'some_api_secret') }
|
157
|
+
let(:order) { FactoryBot.create(:order) }
|
158
|
+
|
159
|
+
it 'should update comment for order' do
|
160
|
+
put_with_signature client, :update, path: { order_id: order.id }, comment: { content: 'Some value' }
|
161
|
+
end
|
162
|
+
end
|
163
|
+
```
|
164
|
+
|
165
|
+
## Development
|
166
|
+
|
167
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
168
|
+
|
169
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
170
|
+
|
171
|
+
## Contributing
|
172
|
+
|
173
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/api_sigv2.
|
174
|
+
|
175
|
+
## License
|
176
|
+
|
177
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/api_sigv2.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'api_sigv2/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'galetahub-api-sigv2'
|
9
|
+
spec.version = ApiSigv2::VERSION
|
10
|
+
spec.authors = ['Igor Galeta']
|
11
|
+
spec.email = ['galeta.igor@gmail.com']
|
12
|
+
|
13
|
+
spec.summary = 'Sign API requests with HMAC signature'
|
14
|
+
spec.homepage = 'https://github.com/galetahub/api_signature'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_development_dependency 'guard-rspec', '~> 4.7', '>= 4.7.3'
|
25
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
26
|
+
spec.add_development_dependency 'rb-fsevent', '0.9.8'
|
27
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
28
|
+
end
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "api_sigv2"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/lib/api_sigv2.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api_sigv2/version'
|
4
|
+
require 'api_sigv2/configuration'
|
5
|
+
|
6
|
+
module ApiSigv2
|
7
|
+
autoload :Builder, 'api_sigv2/builder'
|
8
|
+
autoload :Validator, 'api_sigv2/validator'
|
9
|
+
autoload :Signer, 'api_sigv2/signer'
|
10
|
+
autoload :Signature, 'api_sigv2/signature'
|
11
|
+
autoload :AuthHeader, 'api_sigv2/auth_header'
|
12
|
+
autoload :Utils, 'api_sigv2/utils'
|
13
|
+
|
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
|
25
|
+
|
26
|
+
# @example
|
27
|
+
# ApiSigv2.setup do |config|
|
28
|
+
# config.signature_ttl = 2.minutes
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
def self.setup
|
32
|
+
yield configuration
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSigv2
|
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
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
module ApiSigv2
|
6
|
+
class Builder
|
7
|
+
attr_reader :settings
|
8
|
+
|
9
|
+
SPLITTER = "\n"
|
10
|
+
|
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
|
19
|
+
|
20
|
+
def uri
|
21
|
+
@uri ||= extract_uri
|
22
|
+
end
|
23
|
+
|
24
|
+
def host
|
25
|
+
@host ||= extract_host_from_uri
|
26
|
+
end
|
27
|
+
|
28
|
+
def headers
|
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] || '')
|
46
|
+
end
|
47
|
+
|
48
|
+
def build_sign_headers(apply_checksum_header = false)
|
49
|
+
@sign_headers = {
|
50
|
+
'host' => host,
|
51
|
+
'x-datetime' => datetime
|
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(';')
|
67
|
+
end
|
68
|
+
|
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)
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
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(ApiSigv2.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)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSigv2
|
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 ApiSigv2
|
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 ApiSigv2
|
6
|
+
# The signer requires secret key.
|
7
|
+
#
|
8
|
+
# signer = ApiSigv2::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] || ApiSigv2.configuration.signature_header
|
91
|
+
end
|
92
|
+
|
93
|
+
def service
|
94
|
+
@options[:service] || ApiSigv2.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
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'api_sigv2/spec_support/path_builder'
|
4
|
+
require 'api_sigv2/spec_support/headers_builder'
|
5
|
+
|
6
|
+
module ApiSigv2
|
7
|
+
module SpecSupport
|
8
|
+
module Helper
|
9
|
+
include Rack::Test::Methods
|
10
|
+
|
11
|
+
def app
|
12
|
+
Rails.app_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_with_signature(client, *args)
|
16
|
+
with_signature(:get, client.api_key, client.api_secret, *args)
|
17
|
+
end
|
18
|
+
|
19
|
+
def post_with_signature(client, *args)
|
20
|
+
with_signature(:post, client.api_key, client.api_secret, *args)
|
21
|
+
end
|
22
|
+
|
23
|
+
def put_with_signature(client, *args)
|
24
|
+
with_signature(:put, client.api_key, client.api_secret, *args)
|
25
|
+
end
|
26
|
+
|
27
|
+
alias patch_with_signature put_with_signature
|
28
|
+
|
29
|
+
def delete_with_signature(client, *args)
|
30
|
+
with_signature(:delete, client.api_key, client.api_secret, *args)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def with_signature(http_method, api_key, secret, action_name, params = {})
|
36
|
+
custom_headers = (params.delete(:headers) || {})
|
37
|
+
path = PathBuilder.new(controller, action_name, params).path
|
38
|
+
|
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)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSigv2
|
4
|
+
module SpecSupport
|
5
|
+
class PathBuilder
|
6
|
+
attr_reader :controller, :action_name, :params
|
7
|
+
|
8
|
+
PRIMARY_KEYS = [:id, :token].freeze
|
9
|
+
|
10
|
+
def initialize(controller, action_name, params = {})
|
11
|
+
@controller = controller
|
12
|
+
@action_name = action_name
|
13
|
+
@params = params
|
14
|
+
end
|
15
|
+
|
16
|
+
def path
|
17
|
+
if params[:path].present?
|
18
|
+
hash = params.delete(:path)
|
19
|
+
url_options.merge!(hash)
|
20
|
+
params.merge!(hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
controller.url_for(url_options)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def url_options
|
29
|
+
@url_options ||= {
|
30
|
+
action: action_name,
|
31
|
+
controller: controller.controller_path,
|
32
|
+
only_path: true
|
33
|
+
}.merge(key_options || {})
|
34
|
+
end
|
35
|
+
|
36
|
+
def key_options
|
37
|
+
key = (params.keys.map(&:to_sym) & PRIMARY_KEYS).first
|
38
|
+
{ key => params[key] } if params[key].present?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
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 ApiSigv2
|
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 ||= ApiSigv2.configuration.datetime_format
|
115
|
+
DateTime.strptime(value, format)
|
116
|
+
rescue ArgumentError => _e
|
117
|
+
nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ApiSigv2
|
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 = ApiSigv2::Validator.new(request, uri_escape_path: true)
|
18
|
+
# validator.access_key # get key from request headers
|
19
|
+
# validator.valid?('secret_key')
|
20
|
+
#
|
21
|
+
class Validator
|
22
|
+
attr_reader :request
|
23
|
+
|
24
|
+
def initialize(request, options = {})
|
25
|
+
@request = request
|
26
|
+
@options = options
|
27
|
+
end
|
28
|
+
|
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)
|
59
|
+
end
|
60
|
+
|
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
|
+
)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
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] || ApiSigv2.configuration.signature_header
|
81
|
+
end
|
82
|
+
|
83
|
+
def timestamp
|
84
|
+
@timestamp ||= Utils.safe_parse_datetime(headers['x-datetime'])
|
85
|
+
end
|
86
|
+
|
87
|
+
def headers
|
88
|
+
@headers ||= Utils.normalize_keys(request[:headers])
|
89
|
+
end
|
90
|
+
|
91
|
+
def ttl_range
|
92
|
+
to = Time.now.utc
|
93
|
+
from = to - ApiSigv2.configuration.signature_ttl
|
94
|
+
|
95
|
+
from..to
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
metadata
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: galetahub-api-sigv2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Igor Galeta
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-01-21 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: guard-rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.7'
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 4.7.3
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
requirements:
|
27
|
+
- - "~>"
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '4.7'
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 4.7.3
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: rake
|
35
|
+
requirement: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '10.0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '10.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: rb-fsevent
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - '='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 0.9.8
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - '='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.9.8
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: rspec
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3.0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.0'
|
75
|
+
description:
|
76
|
+
email:
|
77
|
+
- galeta.igor@gmail.com
|
78
|
+
executables: []
|
79
|
+
extensions: []
|
80
|
+
extra_rdoc_files: []
|
81
|
+
files:
|
82
|
+
- ".gitignore"
|
83
|
+
- ".rspec"
|
84
|
+
- ".rubocop.yml"
|
85
|
+
- ".ruby-version"
|
86
|
+
- ".travis.yml"
|
87
|
+
- Gemfile
|
88
|
+
- Guardfile
|
89
|
+
- LICENSE.txt
|
90
|
+
- README.md
|
91
|
+
- Rakefile
|
92
|
+
- api_sigv2.gemspec
|
93
|
+
- bin/console
|
94
|
+
- bin/setup
|
95
|
+
- lib/api_sigv2.rb
|
96
|
+
- lib/api_sigv2/auth_header.rb
|
97
|
+
- lib/api_sigv2/builder.rb
|
98
|
+
- lib/api_sigv2/configuration.rb
|
99
|
+
- lib/api_sigv2/signature.rb
|
100
|
+
- lib/api_sigv2/signer.rb
|
101
|
+
- lib/api_sigv2/spec_support/helper.rb
|
102
|
+
- lib/api_sigv2/spec_support/path_builder.rb
|
103
|
+
- lib/api_sigv2/utils.rb
|
104
|
+
- lib/api_sigv2/validator.rb
|
105
|
+
- lib/api_sigv2/version.rb
|
106
|
+
homepage: https://github.com/galetahub/api_signature
|
107
|
+
licenses:
|
108
|
+
- MIT
|
109
|
+
metadata: {}
|
110
|
+
post_install_message:
|
111
|
+
rdoc_options: []
|
112
|
+
require_paths:
|
113
|
+
- lib
|
114
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
115
|
+
requirements:
|
116
|
+
- - ">="
|
117
|
+
- !ruby/object:Gem::Version
|
118
|
+
version: '0'
|
119
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
requirements: []
|
125
|
+
rubyforge_project:
|
126
|
+
rubygems_version: 2.7.6.2
|
127
|
+
signing_key:
|
128
|
+
specification_version: 4
|
129
|
+
summary: Sign API requests with HMAC signature
|
130
|
+
test_files: []
|