rack-signature 0.0.8 → 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/revans/rack-signature.png)](https://travis-ci.org/revans/rack-signature)
|
2
2
|
[![Code Climate](https://codeclimate.com/badge.png)](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
|
+
]}
|