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 CHANGED
@@ -16,3 +16,4 @@ test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
18
  .DS_Store
19
+ bin/
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
- use Rack::Signature, 'your-shared-key'
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
 
@@ -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
- create_request_message
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 sort_query_params
25
- request.params.sort.map { |param| param.join('=') }
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 canonicalized_query_params
29
- sort_query_params.join('&')
44
+ def empty_form?
45
+ form_vars.nil? || form_vars == ''
30
46
  end
31
47
 
32
- def create_request_message
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
- canonicalized_query_params
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
- # def generate_shared_token; ::SecureRandom.hex(8); end
10
- def generate_shared_token; "a8a5ac6e39f1f5cd"; end
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 = ::Rack::MockRequest.env_for(uri, opts)
27
- msg = stringify_request_message(env)
28
- sig = hmac_message(key, msg)
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
- [uri, opts, query_params, env, sig, msg]
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
- invalid_signature
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
- received_signature = env["X-Auth-Sig"]
44
- expected_signature = compute_signature(env)
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
- expected_signature == received_signature
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
- return '' if token.nil? || token == ''
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
@@ -1,8 +1,8 @@
1
1
  module Rack
2
2
  module Signature
3
3
  MAJOR = 0
4
- MINOR = 0
5
- PATCH = 8
4
+ MINOR = 1
5
+ PATCH = 2
6
6
 
7
7
  def self.version
8
8
  [MAJOR, MINOR, PATCH].join('.')
@@ -22,4 +22,5 @@ Gem::Specification.new do |gem|
22
22
  gem.add_development_dependency 'minitest'
23
23
  gem.add_development_dependency 'rack-test'
24
24
  gem.add_development_dependency 'rake'
25
+ gem.add_development_dependency 'active_support'
25
26
  end
@@ -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
+ ]}