rack-signature 0.0.8 → 0.1.2
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.
- data/.gitignore +1 -0
- data/README.md +50 -1
- data/lib/rack/signature.rb +4 -0
- data/lib/rack/signature/build_message.rb +38 -7
- data/lib/rack/signature/build_post_body.rb +28 -0
- data/lib/rack/signature/deep_merge.rb +57 -0
- data/lib/rack/signature/sort_query_params.rb +45 -0
- data/lib/rack/signature/test_helpers.rb +19 -14
- data/lib/rack/signature/verify.rb +42 -11
- data/lib/rack/signature/version.rb +2 -2
- data/rack-signature.gemspec +1 -0
- data/test/array_of_hashes.json +32 -0
- data/test/build_message_test.rb +40 -4
- data/test/build_post_body_test.rb +24 -0
- data/test/data.json +61 -0
- data/test/deep_merge_test.rb +76 -0
- data/test/ordered_json_data.json +1 -0
- data/test/sort_query_params_test.rb +253 -0
- data/test/test_helper.rb +59 -0
- data/test/verify_test.rb +237 -57
- metadata +34 -4
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -1,6 +1,11 @@
|
|
1
1
|
[](https://travis-ci.org/revans/rack-signature)
|
2
2
|
[](https://codeclimate.com/github/revans/rack-signature)
|
3
3
|
|
4
|
+
# This is currently still being worked on and does have several known bugs.
|
5
|
+
|
6
|
+
This is an alpha release, a pre 1.0 version. If you use this, be aware it's in
|
7
|
+
its infancy.
|
8
|
+
|
4
9
|
# Rack::Signature
|
5
10
|
|
6
11
|
Rack Middleware used to verify signed requests.
|
@@ -21,9 +26,53 @@ Or install it yourself as:
|
|
21
26
|
|
22
27
|
## Usage
|
23
28
|
|
29
|
+
This is meant to be added to a Rails initializer like so:
|
30
|
+
|
24
31
|
```ruby
|
25
|
-
|
32
|
+
Rails.application.config.middleware.use Rack::Signature,
|
33
|
+
klass: ClassWithSharedKey,
|
34
|
+
method: 'method_within_class',
|
35
|
+
header_token: 'http header used to hold the api key'
|
26
36
|
```
|
37
|
+
### Overview
|
38
|
+
|
39
|
+
This gem is assumed to be used within a rails application. It computes the HMAC
|
40
|
+
Signature internally and only sends a (single) request over the network when
|
41
|
+
Signatures fail to match; sending a 401. Otherwise, it makes no requests - only
|
42
|
+
accepts incoming JSON Api requests.
|
43
|
+
|
44
|
+
|
45
|
+
This gem will build an HMAC Signature based off an incoming request made to its
|
46
|
+
JSON Api initiated by some external client. Once it builds the "expected" HMAC
|
47
|
+
Signature, it will compare its Signature against the Signature that was sent by
|
48
|
+
the external client. If they match, the request is allowed to continue to the
|
49
|
+
Rails application. If it fails, this gem will send back a 401 response from its
|
50
|
+
own internal Rack application.
|
51
|
+
|
52
|
+
|
53
|
+
### Options explained:
|
54
|
+
|
55
|
+
#### klass
|
56
|
+
|
57
|
+
The name of the class within the rails application that can be used to query for
|
58
|
+
a model's shared key.
|
59
|
+
|
60
|
+
It is assumed that each consumer has it's own unique shared key; similar to
|
61
|
+
Oauth.
|
62
|
+
|
63
|
+
#### method
|
64
|
+
|
65
|
+
The method within the +klass+ that will be called to request a shared key to
|
66
|
+
build the HMAC. This is a class level method.
|
67
|
+
|
68
|
+
#### header_token
|
69
|
+
|
70
|
+
This is a bad name. It will be changed.
|
71
|
+
|
72
|
+
This is the name of the HTTP Header that holds the Api Key that is associated
|
73
|
+
with the consumer's account. It is used as the authentication as well as a way
|
74
|
+
to get the consumer's account to retreive the +shared key+.
|
75
|
+
|
27
76
|
|
28
77
|
## Contributing
|
29
78
|
|
data/lib/rack/signature.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
require_relative 'signature/version'
|
4
|
+
require_relative 'signature/build_post_body'
|
2
5
|
require_relative 'signature/build_message'
|
3
6
|
require_relative 'signature/hmac_signature'
|
7
|
+
require_relative 'signature/sort_query_params'
|
4
8
|
require_relative 'signature/verify'
|
5
9
|
|
6
10
|
module Rack
|
@@ -1,4 +1,7 @@
|
|
1
1
|
require 'rack/request'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
require_relative 'build_post_body'
|
2
5
|
|
3
6
|
module Rack
|
4
7
|
module Signature
|
@@ -16,24 +19,52 @@ module Rack
|
|
16
19
|
end
|
17
20
|
|
18
21
|
def build!
|
19
|
-
|
22
|
+
read_request_and_build_message
|
23
|
+
end
|
24
|
+
|
25
|
+
def query
|
26
|
+
sort_params
|
20
27
|
end
|
21
28
|
|
22
29
|
private
|
23
30
|
|
24
|
-
def
|
25
|
-
|
31
|
+
def read_request_and_build_message
|
32
|
+
params = sort_params
|
33
|
+
build_request_message(params)
|
34
|
+
end
|
35
|
+
|
36
|
+
def sort_params
|
37
|
+
empty_form? ? for_query_string(request.params) : for_post_body(form_vars)
|
38
|
+
end
|
39
|
+
|
40
|
+
def form_vars
|
41
|
+
@form_vars ||= read_rack_input
|
26
42
|
end
|
27
43
|
|
28
|
-
def
|
29
|
-
|
44
|
+
def empty_form?
|
45
|
+
form_vars.nil? || form_vars == ''
|
30
46
|
end
|
31
47
|
|
32
|
-
def
|
48
|
+
def for_query_string(params)
|
49
|
+
@sorted_params ||= params.sort.map { |p| p.join('=') }.join('&')
|
50
|
+
end
|
51
|
+
|
52
|
+
def for_post_body(params)
|
53
|
+
@sorted_json ||= BuildPostBody.new(params).sorted_json
|
54
|
+
end
|
55
|
+
|
56
|
+
def build_request_message(params)
|
33
57
|
request.request_method.upcase +
|
34
58
|
request.path_info.downcase +
|
35
59
|
request.host.downcase +
|
36
|
-
|
60
|
+
params
|
61
|
+
end
|
62
|
+
|
63
|
+
def read_rack_input
|
64
|
+
form_vars = request.env['rack.input'].read
|
65
|
+
form_vars = JSON.parse(form_vars) rescue form_vars
|
66
|
+
request.env['rack.input'].rewind
|
67
|
+
form_vars
|
37
68
|
end
|
38
69
|
|
39
70
|
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'rack/request'
|
2
|
+
require 'json'
|
3
|
+
require_relative 'sort_query_params'
|
4
|
+
|
5
|
+
module Rack
|
6
|
+
module Signature
|
7
|
+
class BuildPostBody
|
8
|
+
attr_reader :hash
|
9
|
+
|
10
|
+
def initialize(hash)
|
11
|
+
@hash = hash
|
12
|
+
end
|
13
|
+
|
14
|
+
def sorted_json
|
15
|
+
JSON.generate(sort_post_body)
|
16
|
+
end
|
17
|
+
|
18
|
+
def sort_post_body
|
19
|
+
SortQueryParams.new(parse_query).order
|
20
|
+
end
|
21
|
+
|
22
|
+
def parse_query
|
23
|
+
::Rack::Utils.parse_query(hash) rescue hash
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Rack
|
2
|
+
module Signature
|
3
|
+
class DeepMerge
|
4
|
+
attr_reader :hash
|
5
|
+
def initialize(hash)
|
6
|
+
@hash = hash
|
7
|
+
end
|
8
|
+
|
9
|
+
def merge!
|
10
|
+
deep_merge(hash).chomp('&')
|
11
|
+
end
|
12
|
+
|
13
|
+
# merge deep_merge & merge_hash
|
14
|
+
def deep_merge(object, key = nil)
|
15
|
+
object.each_with_object('') do |array, string|
|
16
|
+
string << send_to_method( array, key )
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def send_to_method(object, keys)
|
23
|
+
key,values = bind_variables(object, keys)
|
24
|
+
continue_merge_or_complete(values, key)
|
25
|
+
end
|
26
|
+
|
27
|
+
def bind_variables(object, keys)
|
28
|
+
if object.first.is_a?(String)
|
29
|
+
key, values = build_key(keys, object.first), object.last
|
30
|
+
else
|
31
|
+
key, values = build_key(keys), object
|
32
|
+
end
|
33
|
+
[key,values]
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_key(keys, keychain = nil)
|
37
|
+
key = keychain if keys.nil? || keys.empty? # root node
|
38
|
+
key ||= hash_key(keys) if keychain.nil? # nested array
|
39
|
+
key ||= hash_key(keys, keychain) if keychain.is_a?(String) # nested hash
|
40
|
+
key
|
41
|
+
end
|
42
|
+
|
43
|
+
def hash_key(orig_key, key = nil)
|
44
|
+
"#{orig_key}[#{key}]"
|
45
|
+
end
|
46
|
+
|
47
|
+
def continue_merge_or_complete(object, key)
|
48
|
+
if object.is_a?(Hash) || object.is_a?(Array)
|
49
|
+
deep_merge(object, key)
|
50
|
+
else
|
51
|
+
[key, object].join('=') << '&'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Rack
|
2
|
+
module Signature
|
3
|
+
class SortQueryParams
|
4
|
+
|
5
|
+
def initialize(object)
|
6
|
+
@object = object
|
7
|
+
end
|
8
|
+
|
9
|
+
def order
|
10
|
+
deep_sort(@object)
|
11
|
+
end
|
12
|
+
|
13
|
+
def deep_sort(object)
|
14
|
+
if object.is_a?(Array)
|
15
|
+
|
16
|
+
deep_array_sort(object)
|
17
|
+
elsif object.is_a?(Hash)
|
18
|
+
|
19
|
+
deep_hash_sort(object)
|
20
|
+
else
|
21
|
+
object
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def deep_hash_sort(object)
|
26
|
+
return object unless object.is_a?(Hash)
|
27
|
+
hash = Hash.new
|
28
|
+
object.each { |k,v| hash[k] = deep_sort(v) }
|
29
|
+
sorted = hash.sort { |a,b| a[0].to_s <=> b[0].to_s }
|
30
|
+
hash.class[sorted]
|
31
|
+
end
|
32
|
+
|
33
|
+
def deep_array_sort(object)
|
34
|
+
object.map do |value|
|
35
|
+
if value.is_a?(Hash)
|
36
|
+
deep_hash_sort(value)
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,13 +1,25 @@
|
|
1
1
|
require_relative 'build_message'
|
2
2
|
require_relative 'hmac_signature'
|
3
|
+
require_relative 'sort_query_params'
|
4
|
+
require 'rack'
|
5
|
+
require 'json'
|
3
6
|
|
4
7
|
module Rack
|
5
8
|
module Signature
|
6
9
|
module TestHelpers
|
7
|
-
include Rack::Test::Methods
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
+
def convert_hash_to_string(params)
|
12
|
+
params.map { |p| p.join('=')}.join('&')
|
13
|
+
end
|
14
|
+
|
15
|
+
def hash_to_sorted_json(obj)
|
16
|
+
sorted_hash = SortQueryParams.new(obj).order
|
17
|
+
JSON.generate(sorted_hash)
|
18
|
+
end
|
19
|
+
|
20
|
+
def sorted_json_to_hash(obj)
|
21
|
+
JSON.parse(obj)
|
22
|
+
end
|
11
23
|
|
12
24
|
def stringify_request_message(env)
|
13
25
|
::Rack::Signature::BuildMessage.new(env).build!
|
@@ -17,19 +29,12 @@ module Rack
|
|
17
29
|
::Rack::Signature::HmacSignature.new(key, message).sign
|
18
30
|
end
|
19
31
|
|
20
|
-
def expected_signature(shared_key, env)
|
21
|
-
msg = stringify_request_message(env)
|
22
|
-
hmac_message(shared_key, msg)
|
23
|
-
end
|
24
|
-
|
25
32
|
def setup_request(uri, opts, key)
|
26
|
-
env
|
27
|
-
msg
|
28
|
-
sig
|
29
|
-
req = Rack::Request.new(env)
|
30
|
-
query_params = req.params
|
33
|
+
env = ::Rack::MockRequest.env_for(uri, opts)
|
34
|
+
msg = stringify_request_message(env)
|
35
|
+
sig = hmac_message(key, msg)
|
31
36
|
|
32
|
-
|
37
|
+
{ signature: sig, message: msg, env: env, key: key }
|
33
38
|
end
|
34
39
|
|
35
40
|
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# encoding: UTF-8
|
1
2
|
require_relative 'hmac_signature'
|
2
3
|
|
3
4
|
# A Rack app to verify requests based on a computed signature passed within the
|
@@ -23,27 +24,32 @@ module Rack
|
|
23
24
|
end
|
24
25
|
|
25
26
|
def call(env)
|
27
|
+
dup._call(env)
|
28
|
+
end
|
29
|
+
|
30
|
+
def _call(env)
|
26
31
|
if signature_is_valid?(env)
|
27
32
|
@app.call(env)
|
28
33
|
else
|
29
|
-
|
34
|
+
status, headers, body = @app.call(env)
|
35
|
+
body.close if body.respond_to?(:close)
|
36
|
+
[401, {'CONTENT_TYPE' => 'application/json', :charset => 'utf-8'}, ['Access Denied']]
|
30
37
|
end
|
31
38
|
end
|
32
39
|
|
33
40
|
private
|
34
41
|
|
35
|
-
# if the signature is invalid we send back this Rack app
|
36
|
-
def invalid_signature
|
37
|
-
[403, {'Content-Type' => 'text/html'}, 'Invalid Signature']
|
38
|
-
end
|
39
|
-
|
40
42
|
# compares the received Signature against what the Signature should be
|
41
43
|
# (computed signature)
|
42
44
|
def signature_is_valid?(env)
|
43
|
-
|
44
|
-
|
45
|
+
return true if html_request?(env)
|
46
|
+
|
47
|
+
# grab and compute the X-AUTH-SIG
|
48
|
+
signature_sent = env["HTTP_X_AUTH_SIG"]
|
49
|
+
actual_signature = compute_signature(env)
|
45
50
|
|
46
|
-
|
51
|
+
# are they the same?
|
52
|
+
signature_sent.to_s == actual_signature.to_s
|
47
53
|
end
|
48
54
|
|
49
55
|
# builds the request message and tells HmacSignature to sign the message
|
@@ -56,8 +62,33 @@ module Rack
|
|
56
62
|
# app. This will eventually need to be a rack app itself
|
57
63
|
def shared_key(env)
|
58
64
|
token = env[options[:header_token]]
|
59
|
-
|
60
|
-
options[:klass].send(options[:method].to_s, token)
|
65
|
+
|
66
|
+
shared_token = options[:klass].send(options[:method].to_s, token)
|
67
|
+
shared_token.to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
# we only want to use this if the request is an api request
|
71
|
+
def html_request?(env)
|
72
|
+
debug(env) if options[:debug]
|
73
|
+
(env['CONTENT_TYPE'] || "").to_s !~ /json/i
|
74
|
+
end
|
75
|
+
|
76
|
+
def debug(env)
|
77
|
+
builder = BuildMessage.new(env)
|
78
|
+
log "WHAT MODEL WILL BE CALLED: #{options[:klass]}##{options[:method]} pulling api token from #{options[:header_token]} which is #{env[options[:header_token]]}"
|
79
|
+
log "CALL RAILS MODEL: #{options[:klass].send(options[:method].to_s, (env[options[:header_token]])).inspect}"
|
80
|
+
log "SHARED_KEY from Rails: #{shared_key(env).inspect}"
|
81
|
+
log "CONTENT_TYPE of request: #{env['CONTENT_TYPE'].inspect}"
|
82
|
+
log "QUERY SENT: #{builder.query.inspect}"
|
83
|
+
log "MESSAGE built by rails: #{builder.build!.inspect}"
|
84
|
+
log "HMAC built by rails: #{HmacSignature.new(shared_key(env), builder.build!).sign.inspect}"
|
85
|
+
log "HMAC received from client #{env['HTTP_X_AUTH_SIG'].inspect}"
|
86
|
+
log "API KEY received from client #{env['HTTP_LOCKER_API_KEY'].inspect}"
|
87
|
+
log "RACK ENV: #{env.inspect}"
|
88
|
+
end
|
89
|
+
|
90
|
+
def log(msg)
|
91
|
+
options[:logger].info(msg)
|
61
92
|
end
|
62
93
|
|
63
94
|
end
|
data/rack-signature.gemspec
CHANGED
@@ -0,0 +1,32 @@
|
|
1
|
+
{"club_swing_data": [
|
2
|
+
{
|
3
|
+
"shot": 1,
|
4
|
+
"club": "driver",
|
5
|
+
"mode": "manual",
|
6
|
+
"ball_speed": "130.0",
|
7
|
+
"launch_angle": "10.0",
|
8
|
+
"deviation_angle": "0.0",
|
9
|
+
"backspin": "3000.0",
|
10
|
+
"slidespin": "0.0"
|
11
|
+
},
|
12
|
+
{
|
13
|
+
"shot": 2,
|
14
|
+
"club": "driver",
|
15
|
+
"mode": "manual",
|
16
|
+
"ball_speed": "130.0",
|
17
|
+
"launch_angle": "10.0",
|
18
|
+
"deviation_angle": "0.0",
|
19
|
+
"backspin": "3000.0",
|
20
|
+
"slidespin": "0.0"
|
21
|
+
},
|
22
|
+
{
|
23
|
+
"shot": 3,
|
24
|
+
"club": "driver",
|
25
|
+
"mode": "manual",
|
26
|
+
"ball_speed": "130.0",
|
27
|
+
"launch_angle": "10.0",
|
28
|
+
"deviation_angle": "0.0",
|
29
|
+
"backspin": "3000.0",
|
30
|
+
"slidespin": "0.0"
|
31
|
+
}
|
32
|
+
]}
|