galetahub-api-sigv2 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|