grape_api_signature 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.consolerc +3 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.rubocop.yml +21 -0
- data/.rubocop_todo.yml +0 -0
- data/.travis.yml +6 -0
- data/.versions.conf +4 -0
- data/Gemfile +13 -0
- data/LICENSE.txt +22 -0
- data/README.md +146 -0
- data/Rakefile +14 -0
- data/app/assets/javascripts/aws-signature.js.coffee +177 -0
- data/grape_api_signature.gemspec +54 -0
- data/lib/grape_api_signature/authorization.rb +79 -0
- data/lib/grape_api_signature/aws_auth_parser.rb +57 -0
- data/lib/grape_api_signature/aws_authorization.rb +40 -0
- data/lib/grape_api_signature/aws_digester.rb +27 -0
- data/lib/grape_api_signature/aws_request.rb +63 -0
- data/lib/grape_api_signature/aws_signer.rb +76 -0
- data/lib/grape_api_signature/middleware/auth.rb +105 -0
- data/lib/grape_api_signature/middleware/grape_auth.rb +44 -0
- data/lib/grape_api_signature/rails/engine.rb +6 -0
- data/lib/grape_api_signature/rspec.rb +49 -0
- data/lib/grape_api_signature/version.rb +3 -0
- data/lib/grape_api_signature.rb +21 -0
- data/spec/acceptance/.gitkeep +0 -0
- data/spec/acceptance/lib/grape_api_signature/aws_request_spec.rb +39 -0
- data/spec/acceptance/lib/grape_api_signature/aws_signer_spec.rb +54 -0
- data/spec/acceptance/lib/grape_api_signature/middleware/auth_spec.rb +60 -0
- data/spec/acceptance/lib/grape_api_signature/middleware/grape_auth_spec.rb +83 -0
- data/spec/acceptance/support/.keep +0 -0
- data/spec/acceptance/support/api.rb +5 -0
- data/spec/acceptance/support/aws_helper.rb +30 -0
- data/spec/acceptance/support/feature.rb +31 -0
- data/spec/acceptance_spec_helper.rb +3 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.authz +1 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.creq +9 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.req +7 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.sreq +8 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.sts +4 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-multiline.req +7 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.authz +1 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.creq +9 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.req +8 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.sreq +9 -0
- data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.sts +4 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.authz +1 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.creq +8 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.req +4 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.sts +4 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.authz +1 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.creq +8 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.req +4 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.creq +9 -0
- data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.req +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.sreq +6 -0
- data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-relative.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slash.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slashes.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slashes.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slashes.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slashes.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-slashes.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-space.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-space.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-space.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-space.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-space.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-unreserved.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-unreserved.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-unreserved.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-unreserved.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-unreserved.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-utf8.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-utf8.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-utf8.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-utf8.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-utf8.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/get-vanilla.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.creq +9 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.req +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.sreq +6 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.creq +9 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.req +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.sreq +6 -0
- data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla.creq +8 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla.req +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla.sreq +5 -0
- data/spec/fixtures/aws4_test_suite/pass/post-vanilla.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.creq +9 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.req +6 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.sreq +7 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.sts +4 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.authz +1 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.creq +9 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.req +6 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.sreq +7 -0
- data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.sts +4 -0
- data/spec/integration/.gitkeep +0 -0
- data/spec/integration/support/.keep +0 -0
- data/spec/integration_spec_helper.rb +3 -0
- data/spec/spec_helper.rb +45 -0
- data/spec/support/.gitkeep +0 -0
- data/spec/unit/.gitkeep +0 -0
- data/spec/unit/lib/grape_api_signature/authorization_spec.rb +79 -0
- data/spec/unit/lib/grape_api_signature/aws_auth_parser_spec.rb +25 -0
- data/spec/unit/support/.keep +0 -0
- data/spec/unit_spec_helper.rb +3 -0
- data/vendor/assets/javascripts/hmac-sha256.js +18 -0
- metadata +692 -0
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'openssl'
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
module GrapeAPISignature
|
8
|
+
module AWSDigester
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def hexdigest(value)
|
12
|
+
Digest::SHA256.new.update(value).hexdigest
|
13
|
+
end
|
14
|
+
|
15
|
+
def hmac(key, value)
|
16
|
+
OpenSSL::HMAC.digest(digest, key, value)
|
17
|
+
end
|
18
|
+
|
19
|
+
def hexhmac(key, value)
|
20
|
+
OpenSSL::HMAC.hexdigest(digest, key, value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def digest
|
24
|
+
OpenSSL::Digest.new('sha256')
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'openssl'
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
module GrapeAPISignature
|
8
|
+
class AWSRequest
|
9
|
+
RFC8601BASIC = '%Y%m%dT%H%M%SZ'
|
10
|
+
|
11
|
+
attr_accessor :method, :uri, :headers, :body, :service, :digester
|
12
|
+
|
13
|
+
def self.formatted_time(time)
|
14
|
+
time.utc.strftime(RFC8601BASIC)
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(method, uri, headers, body, digester = GrapeAPISignature::AWSDigester)
|
18
|
+
self.method = method.upcase
|
19
|
+
self.uri = uri
|
20
|
+
self.headers = headers.each_with_object({}) { |(key, value), result_hash| result_hash[key.downcase] = value.strip }
|
21
|
+
self.body = body
|
22
|
+
self.service = uri.host.split('.', 2)[0]
|
23
|
+
self.digester = digester
|
24
|
+
end
|
25
|
+
|
26
|
+
def canonical_request
|
27
|
+
[
|
28
|
+
method,
|
29
|
+
clean_path,
|
30
|
+
query_string,
|
31
|
+
headers_as_str + "\n",
|
32
|
+
headers.keys.sort.join(';'),
|
33
|
+
digester.hexdigest(body || '')
|
34
|
+
].join("\n")
|
35
|
+
end
|
36
|
+
|
37
|
+
def datetime
|
38
|
+
date_header = headers['date'] || headers['x-amz-date']
|
39
|
+
self.class.formatted_time(date_header ? Time.parse(date_header) : Time.now)
|
40
|
+
end
|
41
|
+
|
42
|
+
def date
|
43
|
+
datetime[0, 8]
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
def headers_as_str
|
49
|
+
headers.sort.map { |k, v| [k, v].join(':') }.join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def clean_path
|
53
|
+
path = Pathname.new(uri.path).cleanpath.to_s
|
54
|
+
path << '/' if uri.path.end_with?('/') && !path.end_with?('/')
|
55
|
+
path
|
56
|
+
end
|
57
|
+
|
58
|
+
def query_string
|
59
|
+
return nil if uri.query.nil?
|
60
|
+
uri.query.split('&').sort.join('&')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'openssl'
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
module GrapeAPISignature
|
8
|
+
class AWSSigner
|
9
|
+
attr_accessor :access_key, :secret_key, :region, :digester
|
10
|
+
attr_accessor :request
|
11
|
+
|
12
|
+
def initialize(config)
|
13
|
+
self.access_key = config[:access_key]
|
14
|
+
self.secret_key = config[:secret_key]
|
15
|
+
self.region = config[:region]
|
16
|
+
self.digester = config[:digester] || AWSDigester
|
17
|
+
end
|
18
|
+
|
19
|
+
def setup_aws_request(method, uri, headers, body)
|
20
|
+
self.request = AWSRequest.new(method, uri, headers, body, digester)
|
21
|
+
end
|
22
|
+
|
23
|
+
def sign(method, uri, headers, body)
|
24
|
+
setup_aws_request(method, uri, headers, body)
|
25
|
+
|
26
|
+
signed = headers.dup
|
27
|
+
signed['Authorization'] = authorization(headers)
|
28
|
+
signed
|
29
|
+
end
|
30
|
+
|
31
|
+
def signature_only(method, uri, headers, body)
|
32
|
+
setup_aws_request(method, uri, headers, body)
|
33
|
+
signature
|
34
|
+
end
|
35
|
+
|
36
|
+
def authorization(headers)
|
37
|
+
AWSAuthorization.new(access_key, credential_string, signed_headers(headers), signature).to_s
|
38
|
+
end
|
39
|
+
|
40
|
+
def signed_headers(headers)
|
41
|
+
to_sign = headers.keys.map(&:to_s).map(&:downcase)
|
42
|
+
to_sign.delete('authorization')
|
43
|
+
to_sign
|
44
|
+
end
|
45
|
+
|
46
|
+
def signature
|
47
|
+
digester.hexhmac(derived_key, string_to_sign)
|
48
|
+
end
|
49
|
+
|
50
|
+
def derived_key
|
51
|
+
k_date = digester.hmac('AWS4' + secret_key, request.date)
|
52
|
+
k_region = digester.hmac(k_date, region)
|
53
|
+
k_service = digester.hmac(k_region, request.service)
|
54
|
+
|
55
|
+
digester.hmac(k_service, 'aws4_request')
|
56
|
+
end
|
57
|
+
|
58
|
+
def string_to_sign
|
59
|
+
[
|
60
|
+
'AWS4-HMAC-SHA256',
|
61
|
+
request.datetime,
|
62
|
+
credential_string,
|
63
|
+
digester.hexdigest(request.canonical_request)
|
64
|
+
].join("\n")
|
65
|
+
end
|
66
|
+
|
67
|
+
def credential_string
|
68
|
+
[
|
69
|
+
request.date,
|
70
|
+
region,
|
71
|
+
request.service,
|
72
|
+
'aws4_request'
|
73
|
+
].join('/')
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rack/auth/abstract/handler'
|
3
|
+
require 'rack/auth/abstract/request'
|
4
|
+
require 'rack/request'
|
5
|
+
|
6
|
+
module GrapeAPISignature
|
7
|
+
module Middleware
|
8
|
+
class Auth < Rack::Auth::AbstractHandler
|
9
|
+
attr_accessor :app, :max_request_age, :authenticator, :env
|
10
|
+
|
11
|
+
def self.default_authenticator(&block)
|
12
|
+
@default_authenticator = block if block_given?
|
13
|
+
|
14
|
+
@default_authenticator
|
15
|
+
end
|
16
|
+
|
17
|
+
def initialize(app, max_request_age = 900, &authenticator)
|
18
|
+
self.app = app
|
19
|
+
self.authenticator = authenticator || self.class.default_authenticator
|
20
|
+
self.max_request_age = max_request_age
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(env)
|
24
|
+
dup._call(env)
|
25
|
+
end
|
26
|
+
|
27
|
+
def _call(env)
|
28
|
+
self.env = env
|
29
|
+
|
30
|
+
@auth_request = nil
|
31
|
+
@auth = nil
|
32
|
+
@authenticator_result = nil
|
33
|
+
|
34
|
+
return unauthorized unless auth_request.provided?
|
35
|
+
|
36
|
+
return bad_request unless auth_request.aws4?
|
37
|
+
|
38
|
+
if valid?
|
39
|
+
on_valid
|
40
|
+
else
|
41
|
+
unauthorized
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def on_valid
|
48
|
+
env['REMOTE_USER'] = auth.user_id
|
49
|
+
app.call(env)
|
50
|
+
end
|
51
|
+
|
52
|
+
def auth
|
53
|
+
@auth ||= Authorization.new(request.request_method,
|
54
|
+
auth_request.headers.merge('Content-Type' => request.content_type),
|
55
|
+
URI(request.url),
|
56
|
+
auth_request.body,
|
57
|
+
max_request_age)
|
58
|
+
end
|
59
|
+
|
60
|
+
def auth_request
|
61
|
+
@auth_request ||= AuthRequest.new(env)
|
62
|
+
end
|
63
|
+
|
64
|
+
def request
|
65
|
+
auth_request.request
|
66
|
+
end
|
67
|
+
|
68
|
+
def challenge
|
69
|
+
'AWS4-HMAC-SHA256'
|
70
|
+
end
|
71
|
+
|
72
|
+
def valid?
|
73
|
+
secret_key && auth.authentic?(secret_key)
|
74
|
+
end
|
75
|
+
|
76
|
+
def secret_key
|
77
|
+
authenticator_result
|
78
|
+
end
|
79
|
+
|
80
|
+
def authenticator_result
|
81
|
+
@authenticator_result ||= @authenticator.call(auth.user_id, auth.region, auth.service)
|
82
|
+
end
|
83
|
+
|
84
|
+
class AuthRequest < Rack::Auth::AbstractRequest
|
85
|
+
def aws4?
|
86
|
+
'AWS4-HMAC-SHA256'.downcase == scheme.downcase
|
87
|
+
end
|
88
|
+
|
89
|
+
def headers
|
90
|
+
@headers ||= @env.each_with_object({}) do |(key, value), result_hash|
|
91
|
+
key = key.upcase
|
92
|
+
next unless key.to_s.start_with?('HTTP_') && (key.to_s != 'HTTP_VERSION')
|
93
|
+
|
94
|
+
key = key[5..-1].gsub('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
95
|
+
result_hash[key] = value
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def body
|
100
|
+
@body ||= request.body.read.tap { request.body.rewind }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'rack/auth/abstract/handler'
|
3
|
+
require 'rack/auth/abstract/request'
|
4
|
+
require 'rack/request'
|
5
|
+
|
6
|
+
module GrapeAPISignature
|
7
|
+
module Middleware
|
8
|
+
class GrapeAuth < GrapeAPISignature::Middleware::Auth
|
9
|
+
attr_accessor :user_setter
|
10
|
+
|
11
|
+
def initialize(app, max_request_age = 900, user_setter = :'current_user=', &authenticator)
|
12
|
+
super(app, max_request_age, &authenticator)
|
13
|
+
self.user_setter = user_setter
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
def endpoint
|
19
|
+
env['api.endpoint']
|
20
|
+
end
|
21
|
+
|
22
|
+
def on_valid
|
23
|
+
endpoint.send(user_setter, user) if user_setter
|
24
|
+
super
|
25
|
+
end
|
26
|
+
|
27
|
+
def secret_key
|
28
|
+
authenticator_result[:secret_key]
|
29
|
+
end
|
30
|
+
|
31
|
+
def user
|
32
|
+
authenticator_result[:user]
|
33
|
+
end
|
34
|
+
|
35
|
+
def unauthorized(www_authenticate = challenge)
|
36
|
+
endpoint.error!({ error: 'Unauthorized' }, 401, 'WWW-Authenticate' => www_authenticate.to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
def bad_request
|
40
|
+
endpoint.error!({ error: 'Bad Request' }, 400)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module GrapeAPISignature
|
2
|
+
module RSpec
|
3
|
+
# remember the diff request spec vs. controller spec
|
4
|
+
# a controller spec offers a request object, a request spec
|
5
|
+
# doesn't why ever
|
6
|
+
def post_with_auth(path, parameters = nil, headers_or_env = {})
|
7
|
+
headers_or_env.merge! sign_request('POST', path, parameters)
|
8
|
+
|
9
|
+
# convert the body or ActionDispatch::Integration::RequestHelpers
|
10
|
+
# enforces content type application/x-www-form-urlencoded
|
11
|
+
# https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/testing/integration.rb#L286
|
12
|
+
body = parameters.to_json
|
13
|
+
post path, body, headers_or_env.merge('Content-Type' => 'application/json')
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_with_auth(path, parameters = nil, headers_or_env = {})
|
17
|
+
headers_or_env.merge! sign_request('GET', path, parameters)
|
18
|
+
get path, parameters, headers_or_env # .merge('Content-Type' => 'application/json')
|
19
|
+
end
|
20
|
+
|
21
|
+
def sign_request(method, path, parameters = nil)
|
22
|
+
if parameters.present?
|
23
|
+
body = parameters.to_json
|
24
|
+
else
|
25
|
+
body = ''
|
26
|
+
end
|
27
|
+
|
28
|
+
headers_or_env = {
|
29
|
+
'x-amz-date' => ::GrapeAPISignature::AWSRequest.formatted_time(Time.now),
|
30
|
+
'ACCEPT' => 'application/json'
|
31
|
+
}
|
32
|
+
|
33
|
+
signer = ::GrapeAPISignature::AWSSigner.new(
|
34
|
+
access_key: access_key,
|
35
|
+
secret_key: secret_key,
|
36
|
+
region: 'europe'
|
37
|
+
)
|
38
|
+
|
39
|
+
(hostname, port) = host.split(':')
|
40
|
+
|
41
|
+
uri = URI(path)
|
42
|
+
uri.host ||= hostname
|
43
|
+
uri.scheme ||= https? ? 'https' : 'http'
|
44
|
+
uri.port ||= (port || (https? ? 443 : 80)).to_i
|
45
|
+
|
46
|
+
signer.sign(method, uri, headers_or_env, body).each_with_object({}) { |(k, v), new_h| new_h["HTTP_#{k.upcase}"] = v }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'grape_api_signature/version'
|
2
|
+
|
3
|
+
require 'active_support'
|
4
|
+
require 'active_support/core_ext'
|
5
|
+
|
6
|
+
module GrapeAPISignature
|
7
|
+
require 'grape_api_signature/aws_digester'
|
8
|
+
require 'grape_api_signature/aws_request'
|
9
|
+
require 'grape_api_signature/aws_auth_parser'
|
10
|
+
require 'grape_api_signature/aws_signer'
|
11
|
+
require 'grape_api_signature/aws_authorization'
|
12
|
+
require 'grape_api_signature/authorization'
|
13
|
+
|
14
|
+
require 'grape_api_signature/middleware/auth'
|
15
|
+
require 'grape_api_signature/middleware/grape_auth'
|
16
|
+
|
17
|
+
if defined?(Rails)
|
18
|
+
require 'grape_api_signature/rails/engine'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
File without changes
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'acceptance_spec_helper'
|
2
|
+
require 'thin'
|
3
|
+
|
4
|
+
module GrapeAPISignature
|
5
|
+
describe AWSRequest, :aws_helpers do
|
6
|
+
|
7
|
+
Dir[File.join(Spec::Support::AWSHelper.suite_dir, '*.req')].each do |f|
|
8
|
+
|
9
|
+
filename = File.basename(f, '.req')
|
10
|
+
|
11
|
+
let(:id) { 'AKIDEXAMPLE' }
|
12
|
+
let(:region) { 'us-east-1' }
|
13
|
+
let(:host) { 'host' }
|
14
|
+
|
15
|
+
let(:subject) { GrapeAPISignature::AWSRequest.new(request.request_method, uri, headers, body) }
|
16
|
+
|
17
|
+
describe "testcase #{filename}" do
|
18
|
+
let(:request_str) { File.read(f).gsub('http/1', 'HTTP/1') }
|
19
|
+
let(:request) { request_for_str(request_str) }
|
20
|
+
let(:canonical_request) { File.read(File.join(Spec::Support::AWSHelper.suite_dir, "#{filename}.creq")).gsub("\r\n", "\n") }
|
21
|
+
|
22
|
+
let(:uri) do
|
23
|
+
begin
|
24
|
+
URI(request.url)
|
25
|
+
rescue URI::InvalidURIError
|
26
|
+
URI(URI.encode(request.url))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
let(:headers) { headers_from_env(request.env) }
|
30
|
+
let(:body) { request.body.read }
|
31
|
+
|
32
|
+
it 'creates the expected canonical_request' do
|
33
|
+
expect(subject.canonical_request).to eq canonical_request
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'acceptance_spec_helper'
|
2
|
+
require 'thin'
|
3
|
+
|
4
|
+
module GrapeAPISignature
|
5
|
+
describe AWSSigner, :aws_helpers do
|
6
|
+
|
7
|
+
Dir[File.join(Spec::Support::AWSHelper.suite_dir, '*.req')].each do |f|
|
8
|
+
|
9
|
+
filename = File.basename(f, '.req')
|
10
|
+
|
11
|
+
let(:secret_key) { 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY' }
|
12
|
+
let(:id) { 'AKIDEXAMPLE' }
|
13
|
+
let(:region) { 'us-east-1' }
|
14
|
+
let(:host) { 'host' }
|
15
|
+
|
16
|
+
let(:subject)do
|
17
|
+
GrapeAPISignature::AWSSigner.new(
|
18
|
+
access_key: id,
|
19
|
+
secret_key: secret_key,
|
20
|
+
region: region
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe "testcase #{filename}" do
|
25
|
+
let(:request_str) { File.read(f).gsub('http/1', 'HTTP/1') }
|
26
|
+
let(:str_to_sign) { File.read(File.join(Spec::Support::AWSHelper.suite_dir, "#{filename}.sts")).gsub("\r\n", "\n") }
|
27
|
+
let(:auth_header) { File.read(File.join(Spec::Support::AWSHelper.suite_dir, "#{filename}.authz")) }
|
28
|
+
|
29
|
+
let(:request) { request_for_str(request_str) }
|
30
|
+
|
31
|
+
let(:uri) do
|
32
|
+
begin
|
33
|
+
URI(request.url)
|
34
|
+
rescue URI::InvalidURIError
|
35
|
+
URI(URI.encode(request.url))
|
36
|
+
end
|
37
|
+
end
|
38
|
+
let(:headers) { headers_from_env(request.env) }
|
39
|
+
let(:body) { request.body.read }
|
40
|
+
|
41
|
+
it 'creates the expected string to sign' do
|
42
|
+
subject.setup_aws_request(request.request_method, uri, headers, body)
|
43
|
+
expect(subject.string_to_sign).to eq str_to_sign
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'creates the expected signature' do
|
47
|
+
signature = subject.sign(request.request_method, uri, headers, body)
|
48
|
+
expect(signature['Authorization']).to eq auth_header
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'acceptance_spec_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
|
4
|
+
feature 'Authorize a request' do
|
5
|
+
include Rack::Test::Methods
|
6
|
+
include GrapeAPISignature::RSpec
|
7
|
+
|
8
|
+
let(:base_app) { ->(env) { [200, env, 'app'] } }
|
9
|
+
|
10
|
+
let(:app) do
|
11
|
+
app = base_app
|
12
|
+
key = secret_key
|
13
|
+
Rack::Builder.app do
|
14
|
+
use GrapeAPISignature::Middleware::Auth do |*|
|
15
|
+
key
|
16
|
+
end
|
17
|
+
run app
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
let(:secret_key) { '12345678' }
|
22
|
+
let(:access_key) { 'MyUser' }
|
23
|
+
|
24
|
+
let(:hostname) { Rack::Test::DEFAULT_HOST }
|
25
|
+
let(:port) { 80 }
|
26
|
+
let(:host) { "#{hostname}:#{port}" }
|
27
|
+
|
28
|
+
def https?
|
29
|
+
port == 443
|
30
|
+
end
|
31
|
+
|
32
|
+
scenario 'authentication valid' do
|
33
|
+
get_with_auth '/'
|
34
|
+
|
35
|
+
expect(last_response.status).to eq 200
|
36
|
+
expect(last_response.body).to eq 'app'
|
37
|
+
end
|
38
|
+
|
39
|
+
scenario 'authentication not provided' do
|
40
|
+
get '/'
|
41
|
+
|
42
|
+
expect(last_response.status).to eq 401
|
43
|
+
expect(last_response.headers).to have_key 'WWW-Authenticate'
|
44
|
+
expect(last_response.headers['WWW-Authenticate']).to eq 'AWS4-HMAC-SHA256'
|
45
|
+
end
|
46
|
+
|
47
|
+
scenario 'wrong schema' do
|
48
|
+
get '/', nil, 'HTTP_AUTHORIZATION' => 'Basic'
|
49
|
+
|
50
|
+
expect(last_response.status).to eq 400
|
51
|
+
end
|
52
|
+
|
53
|
+
scenario 'wrong signature' do
|
54
|
+
get '/', nil, 'HTTP_AUTHORIZATION' => 'AWS4-HMAC-SHA256 Credential='
|
55
|
+
|
56
|
+
expect(last_response.status).to eq 401
|
57
|
+
expect(last_response.headers).to have_key 'WWW-Authenticate'
|
58
|
+
expect(last_response.headers['WWW-Authenticate']).to eq 'AWS4-HMAC-SHA256'
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'acceptance_spec_helper'
|
2
|
+
require 'rack/test'
|
3
|
+
require 'grape'
|
4
|
+
|
5
|
+
feature 'Authorize a grape api request' do
|
6
|
+
include Rack::Test::Methods
|
7
|
+
include GrapeAPISignature::RSpec
|
8
|
+
|
9
|
+
def https?
|
10
|
+
port == 443
|
11
|
+
end
|
12
|
+
|
13
|
+
let(:api) do
|
14
|
+
key = secret_key
|
15
|
+
|
16
|
+
Class.new(Grape::API) do
|
17
|
+
Grape::Middleware::Auth::Strategies.add(:aws4_auth,
|
18
|
+
GrapeAPISignature::Middleware::GrapeAuth,
|
19
|
+
->(options) { [options[:max_request_age] || 900, options[:user_setter]] })
|
20
|
+
|
21
|
+
GrapeAPISignature::Middleware::GrapeAuth.default_authenticator do |_user_id, _region, _service|
|
22
|
+
{ user: 'its me', secret_key: key }
|
23
|
+
end
|
24
|
+
|
25
|
+
helpers do
|
26
|
+
attr_accessor :current_user
|
27
|
+
end
|
28
|
+
|
29
|
+
auth :aws4_auth,
|
30
|
+
max_request_age: 100,
|
31
|
+
user_setter: :'current_user=',
|
32
|
+
&GrapeAPISignature::Middleware::GrapeAuth.default_authenticator
|
33
|
+
|
34
|
+
get '/aws4_authorized' do
|
35
|
+
'DONE'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
let(:app) do
|
42
|
+
api
|
43
|
+
end
|
44
|
+
|
45
|
+
let(:secret_key) { '12345678' }
|
46
|
+
let(:access_key) { 'MyUser' }
|
47
|
+
|
48
|
+
let(:hostname) { Rack::Test::DEFAULT_HOST }
|
49
|
+
let(:port) { 80 }
|
50
|
+
let(:host) { "#{hostname}:#{port}" }
|
51
|
+
|
52
|
+
scenario 'authentication valid' do
|
53
|
+
get_with_auth '/aws4_authorized'
|
54
|
+
|
55
|
+
expect(last_response.status).to eq 200
|
56
|
+
expect(last_response.body).to eq '"DONE"'
|
57
|
+
end
|
58
|
+
|
59
|
+
scenario 'authentication not provided' do
|
60
|
+
get '/aws4_authorized'
|
61
|
+
|
62
|
+
expect(last_response.status).to eq 401
|
63
|
+
expect(last_response.headers).to have_key 'WWW-Authenticate'
|
64
|
+
expect(last_response.headers['WWW-Authenticate']).to eq 'AWS4-HMAC-SHA256'
|
65
|
+
expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json)
|
66
|
+
end
|
67
|
+
|
68
|
+
scenario 'wrong schema' do
|
69
|
+
get '/aws4_authorized', nil, 'HTTP_AUTHORIZATION' => 'Basic'
|
70
|
+
|
71
|
+
expect(last_response.status).to eq 400
|
72
|
+
expect(last_response.body).to eq({ error: 'Bad Request' }.to_json)
|
73
|
+
end
|
74
|
+
|
75
|
+
scenario 'wrong signature' do
|
76
|
+
get '/aws4_authorized', nil, 'HTTP_AUTHORIZATION' => 'AWS4-HMAC-SHA256 Credential='
|
77
|
+
|
78
|
+
expect(last_response.status).to eq 401
|
79
|
+
expect(last_response.headers).to have_key 'WWW-Authenticate'
|
80
|
+
expect(last_response.headers['WWW-Authenticate']).to eq 'AWS4-HMAC-SHA256'
|
81
|
+
expect(last_response.body).to eq({ error: 'Unauthorized' }.to_json)
|
82
|
+
end
|
83
|
+
end
|
File without changes
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Spec
|
2
|
+
module Support
|
3
|
+
module AWSHelper
|
4
|
+
def headers_from_env(env)
|
5
|
+
headers = {}
|
6
|
+
headers['CONTENT-TYPE'] = env['CONTENT_TYPE'] if env.key?('CONTENT_TYPE')
|
7
|
+
|
8
|
+
env.each_with_object(headers) do |(key, value), result_hash|
|
9
|
+
next unless key.to_s.start_with?('HTTP_') && (key.to_s != 'HTTP_VERSION')
|
10
|
+
|
11
|
+
key = key[5..-1].gsub('_', '-').downcase.gsub(/^.|[-_\s]./) { |x| x.upcase }
|
12
|
+
result_hash[key] = value
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def request_for_str(str)
|
17
|
+
thin_req = Thin::Request.new.tap { |r| r.parse(str) }
|
18
|
+
Rack::Request.new(thin_req.env)
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.suite_dir
|
22
|
+
File.join(File.expand_path(__dir__), '../../fixtures/aws4_test_suite/pass')
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
RSpec.configure do |config|
|
29
|
+
config.include Spec::Support::AWSHelper, :aws_helpers
|
30
|
+
end
|