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 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
+ ]}