grape_api_signature 0.0.1

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.
Files changed (199) hide show
  1. checksums.yaml +7 -0
  2. data/.consolerc +3 -0
  3. data/.gitignore +23 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +21 -0
  6. data/.rubocop_todo.yml +0 -0
  7. data/.travis.yml +6 -0
  8. data/.versions.conf +4 -0
  9. data/Gemfile +13 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +146 -0
  12. data/Rakefile +14 -0
  13. data/app/assets/javascripts/aws-signature.js.coffee +177 -0
  14. data/grape_api_signature.gemspec +54 -0
  15. data/lib/grape_api_signature/authorization.rb +79 -0
  16. data/lib/grape_api_signature/aws_auth_parser.rb +57 -0
  17. data/lib/grape_api_signature/aws_authorization.rb +40 -0
  18. data/lib/grape_api_signature/aws_digester.rb +27 -0
  19. data/lib/grape_api_signature/aws_request.rb +63 -0
  20. data/lib/grape_api_signature/aws_signer.rb +76 -0
  21. data/lib/grape_api_signature/middleware/auth.rb +105 -0
  22. data/lib/grape_api_signature/middleware/grape_auth.rb +44 -0
  23. data/lib/grape_api_signature/rails/engine.rb +6 -0
  24. data/lib/grape_api_signature/rspec.rb +49 -0
  25. data/lib/grape_api_signature/version.rb +3 -0
  26. data/lib/grape_api_signature.rb +21 -0
  27. data/spec/acceptance/.gitkeep +0 -0
  28. data/spec/acceptance/lib/grape_api_signature/aws_request_spec.rb +39 -0
  29. data/spec/acceptance/lib/grape_api_signature/aws_signer_spec.rb +54 -0
  30. data/spec/acceptance/lib/grape_api_signature/middleware/auth_spec.rb +60 -0
  31. data/spec/acceptance/lib/grape_api_signature/middleware/grape_auth_spec.rb +83 -0
  32. data/spec/acceptance/support/.keep +0 -0
  33. data/spec/acceptance/support/api.rb +5 -0
  34. data/spec/acceptance/support/aws_helper.rb +30 -0
  35. data/spec/acceptance/support/feature.rb +31 -0
  36. data/spec/acceptance_spec_helper.rb +3 -0
  37. data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.authz +1 -0
  38. data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.creq +9 -0
  39. data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.req +7 -0
  40. data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.sreq +8 -0
  41. data/spec/fixtures/aws4_test_suite/fail/get-header-key-duplicate.sts +4 -0
  42. data/spec/fixtures/aws4_test_suite/fail/get-header-value-multiline.req +7 -0
  43. data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.authz +1 -0
  44. data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.creq +9 -0
  45. data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.req +8 -0
  46. data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.sreq +9 -0
  47. data/spec/fixtures/aws4_test_suite/fail/get-header-value-order.sts +4 -0
  48. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.authz +1 -0
  49. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.creq +8 -0
  50. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.req +4 -0
  51. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.sreq +5 -0
  52. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-nonunreserved.sts +4 -0
  53. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.authz +1 -0
  54. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.creq +8 -0
  55. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.req +4 -0
  56. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.sreq +5 -0
  57. data/spec/fixtures/aws4_test_suite/fail/post-vanilla-query-space.sts +4 -0
  58. data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.authz +1 -0
  59. data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.creq +9 -0
  60. data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.req +5 -0
  61. data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.sreq +6 -0
  62. data/spec/fixtures/aws4_test_suite/pass/get-header-value-trim.sts +4 -0
  63. data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.authz +1 -0
  64. data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.creq +8 -0
  65. data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.req +4 -0
  66. data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.sreq +5 -0
  67. data/spec/fixtures/aws4_test_suite/pass/get-relative-relative.sts +4 -0
  68. data/spec/fixtures/aws4_test_suite/pass/get-relative.authz +1 -0
  69. data/spec/fixtures/aws4_test_suite/pass/get-relative.creq +8 -0
  70. data/spec/fixtures/aws4_test_suite/pass/get-relative.req +4 -0
  71. data/spec/fixtures/aws4_test_suite/pass/get-relative.sreq +5 -0
  72. data/spec/fixtures/aws4_test_suite/pass/get-relative.sts +4 -0
  73. data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.authz +1 -0
  74. data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.creq +8 -0
  75. data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.req +4 -0
  76. data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.sreq +5 -0
  77. data/spec/fixtures/aws4_test_suite/pass/get-slash-dot-slash.sts +4 -0
  78. data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.authz +1 -0
  79. data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.creq +8 -0
  80. data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.req +4 -0
  81. data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.sreq +5 -0
  82. data/spec/fixtures/aws4_test_suite/pass/get-slash-pointless-dot.sts +4 -0
  83. data/spec/fixtures/aws4_test_suite/pass/get-slash.authz +1 -0
  84. data/spec/fixtures/aws4_test_suite/pass/get-slash.creq +8 -0
  85. data/spec/fixtures/aws4_test_suite/pass/get-slash.req +4 -0
  86. data/spec/fixtures/aws4_test_suite/pass/get-slash.sreq +5 -0
  87. data/spec/fixtures/aws4_test_suite/pass/get-slash.sts +4 -0
  88. data/spec/fixtures/aws4_test_suite/pass/get-slashes.authz +1 -0
  89. data/spec/fixtures/aws4_test_suite/pass/get-slashes.creq +8 -0
  90. data/spec/fixtures/aws4_test_suite/pass/get-slashes.req +4 -0
  91. data/spec/fixtures/aws4_test_suite/pass/get-slashes.sreq +5 -0
  92. data/spec/fixtures/aws4_test_suite/pass/get-slashes.sts +4 -0
  93. data/spec/fixtures/aws4_test_suite/pass/get-space.authz +1 -0
  94. data/spec/fixtures/aws4_test_suite/pass/get-space.creq +8 -0
  95. data/spec/fixtures/aws4_test_suite/pass/get-space.req +4 -0
  96. data/spec/fixtures/aws4_test_suite/pass/get-space.sreq +5 -0
  97. data/spec/fixtures/aws4_test_suite/pass/get-space.sts +4 -0
  98. data/spec/fixtures/aws4_test_suite/pass/get-unreserved.authz +1 -0
  99. data/spec/fixtures/aws4_test_suite/pass/get-unreserved.creq +8 -0
  100. data/spec/fixtures/aws4_test_suite/pass/get-unreserved.req +4 -0
  101. data/spec/fixtures/aws4_test_suite/pass/get-unreserved.sreq +5 -0
  102. data/spec/fixtures/aws4_test_suite/pass/get-unreserved.sts +4 -0
  103. data/spec/fixtures/aws4_test_suite/pass/get-utf8.authz +1 -0
  104. data/spec/fixtures/aws4_test_suite/pass/get-utf8.creq +8 -0
  105. data/spec/fixtures/aws4_test_suite/pass/get-utf8.req +4 -0
  106. data/spec/fixtures/aws4_test_suite/pass/get-utf8.sreq +5 -0
  107. data/spec/fixtures/aws4_test_suite/pass/get-utf8.sts +4 -0
  108. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.authz +1 -0
  109. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.creq +8 -0
  110. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.req +4 -0
  111. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.sreq +5 -0
  112. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-empty-query-key.sts +4 -0
  113. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.authz +1 -0
  114. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.creq +8 -0
  115. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.req +4 -0
  116. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.sreq +5 -0
  117. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key-case.sts +4 -0
  118. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.authz +1 -0
  119. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.creq +8 -0
  120. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.req +4 -0
  121. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.sreq +5 -0
  122. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-key.sts +4 -0
  123. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.authz +1 -0
  124. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.creq +8 -0
  125. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.req +4 -0
  126. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.sreq +5 -0
  127. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-order-value.sts +4 -0
  128. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.authz +1 -0
  129. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.creq +8 -0
  130. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.req +4 -0
  131. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.sreq +5 -0
  132. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query-unreserved.sts +4 -0
  133. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.authz +1 -0
  134. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.creq +8 -0
  135. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.req +4 -0
  136. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.sreq +5 -0
  137. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-query.sts +4 -0
  138. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.authz +1 -0
  139. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.creq +8 -0
  140. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.req +4 -0
  141. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.sreq +5 -0
  142. data/spec/fixtures/aws4_test_suite/pass/get-vanilla-ut8-query.sts +4 -0
  143. data/spec/fixtures/aws4_test_suite/pass/get-vanilla.authz +1 -0
  144. data/spec/fixtures/aws4_test_suite/pass/get-vanilla.creq +8 -0
  145. data/spec/fixtures/aws4_test_suite/pass/get-vanilla.req +4 -0
  146. data/spec/fixtures/aws4_test_suite/pass/get-vanilla.sreq +5 -0
  147. data/spec/fixtures/aws4_test_suite/pass/get-vanilla.sts +4 -0
  148. data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.authz +1 -0
  149. data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.creq +8 -0
  150. data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.req +4 -0
  151. data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.sreq +5 -0
  152. data/spec/fixtures/aws4_test_suite/pass/post-header-key-case.sts +4 -0
  153. data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.authz +1 -0
  154. data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.creq +9 -0
  155. data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.req +5 -0
  156. data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.sreq +6 -0
  157. data/spec/fixtures/aws4_test_suite/pass/post-header-key-sort.sts +4 -0
  158. data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.authz +1 -0
  159. data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.creq +9 -0
  160. data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.req +5 -0
  161. data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.sreq +6 -0
  162. data/spec/fixtures/aws4_test_suite/pass/post-header-value-case.sts +4 -0
  163. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.authz +1 -0
  164. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.creq +8 -0
  165. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.req +4 -0
  166. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.sreq +5 -0
  167. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-empty-query-value.sts +4 -0
  168. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.authz +1 -0
  169. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.creq +8 -0
  170. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.req +4 -0
  171. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.sreq +5 -0
  172. data/spec/fixtures/aws4_test_suite/pass/post-vanilla-query.sts +4 -0
  173. data/spec/fixtures/aws4_test_suite/pass/post-vanilla.authz +1 -0
  174. data/spec/fixtures/aws4_test_suite/pass/post-vanilla.creq +8 -0
  175. data/spec/fixtures/aws4_test_suite/pass/post-vanilla.req +4 -0
  176. data/spec/fixtures/aws4_test_suite/pass/post-vanilla.sreq +5 -0
  177. data/spec/fixtures/aws4_test_suite/pass/post-vanilla.sts +4 -0
  178. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.authz +1 -0
  179. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.creq +9 -0
  180. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.req +6 -0
  181. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.sreq +7 -0
  182. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded-parameters.sts +4 -0
  183. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.authz +1 -0
  184. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.creq +9 -0
  185. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.req +6 -0
  186. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.sreq +7 -0
  187. data/spec/fixtures/aws4_test_suite/pass/post-x-www-form-urlencoded.sts +4 -0
  188. data/spec/integration/.gitkeep +0 -0
  189. data/spec/integration/support/.keep +0 -0
  190. data/spec/integration_spec_helper.rb +3 -0
  191. data/spec/spec_helper.rb +45 -0
  192. data/spec/support/.gitkeep +0 -0
  193. data/spec/unit/.gitkeep +0 -0
  194. data/spec/unit/lib/grape_api_signature/authorization_spec.rb +79 -0
  195. data/spec/unit/lib/grape_api_signature/aws_auth_parser_spec.rb +25 -0
  196. data/spec/unit/support/.keep +0 -0
  197. data/spec/unit_spec_helper.rb +3 -0
  198. data/vendor/assets/javascripts/hmac-sha256.js +18 -0
  199. 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,6 @@
1
+ module GrapeAPISignature
2
+ module Rails
3
+ class Engine < ::Rails::Engine
4
+ end
5
+ end
6
+ 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,3 @@
1
+ module GrapeAPISignature
2
+ VERSION = '0.0.1'
3
+ 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,5 @@
1
+ require 'grape_api_signature/rspec'
2
+
3
+ RSpec.configure do |config|
4
+ config.include GrapeAPISignature::RSpec, type: :request, file_path: /spec\/acceptance\/api/
5
+ end
@@ -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