secure_escrow 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ pkg/*
4
+ log
5
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,21 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'uuid'
6
+ gem 'actionpack'
7
+
8
+ group :development do
9
+ gem 'rspec', '>=2.6.0'
10
+ gem 'spork', '~> 0.9.0.rc9'
11
+ gem 'rake'
12
+ gem 'guard'
13
+ gem 'rb-fsevent'
14
+ gem 'growl_notify'
15
+ gem 'guard-rspec'
16
+ gem 'guard-spork'
17
+ gem 'pry'
18
+ gem 'pry-remote'
19
+ gem 'foreman'
20
+ end
21
+
data/Gemfile.lock ADDED
@@ -0,0 +1,110 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ secure_escrow (0.0.1)
5
+ redis
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ actionpack (3.1.1)
11
+ activemodel (= 3.1.1)
12
+ activesupport (= 3.1.1)
13
+ builder (~> 3.0.0)
14
+ erubis (~> 2.7.0)
15
+ i18n (~> 0.6)
16
+ rack (~> 1.3.2)
17
+ rack-cache (~> 1.1)
18
+ rack-mount (~> 0.8.2)
19
+ rack-test (~> 0.6.1)
20
+ sprockets (~> 2.0.2)
21
+ activemodel (3.1.1)
22
+ activesupport (= 3.1.1)
23
+ builder (~> 3.0.0)
24
+ i18n (~> 0.6)
25
+ activesupport (3.1.1)
26
+ multi_json (~> 1.0)
27
+ builder (3.0.0)
28
+ coderay (0.9.8)
29
+ diff-lcs (1.1.3)
30
+ erubis (2.7.0)
31
+ foreman (0.25.0)
32
+ term-ansicolor (~> 1.0.5)
33
+ thor (>= 0.13.6)
34
+ growl_notify (0.0.3)
35
+ rb-appscript
36
+ guard (0.8.8)
37
+ thor (~> 0.14.6)
38
+ guard-rspec (0.5.2)
39
+ guard (>= 0.8.4)
40
+ guard-spork (0.3.1)
41
+ guard (>= 0.8.4)
42
+ spork (>= 0.8.4)
43
+ hike (1.2.1)
44
+ i18n (0.6.0)
45
+ macaddr (1.5.0)
46
+ systemu (>= 2.4.0)
47
+ method_source (0.6.7)
48
+ ruby_parser (>= 2.3.1)
49
+ multi_json (1.0.3)
50
+ pry (0.9.7.4)
51
+ coderay (~> 0.9.8)
52
+ method_source (~> 0.6.7)
53
+ ruby_parser (>= 2.3.1)
54
+ slop (~> 2.1.0)
55
+ pry-remote (0.1.0)
56
+ pry (~> 0.9.6)
57
+ slop (~> 2.1)
58
+ rack (1.3.5)
59
+ rack-cache (1.1)
60
+ rack (>= 0.4)
61
+ rack-mount (0.8.3)
62
+ rack (>= 1.0.0)
63
+ rack-test (0.6.1)
64
+ rack (>= 1.0)
65
+ rake (0.9.2.2)
66
+ rb-appscript (0.6.1)
67
+ rb-fsevent (0.4.3.1)
68
+ redis (2.2.2)
69
+ rspec (2.7.0)
70
+ rspec-core (~> 2.7.0)
71
+ rspec-expectations (~> 2.7.0)
72
+ rspec-mocks (~> 2.7.0)
73
+ rspec-core (2.7.1)
74
+ rspec-expectations (2.7.0)
75
+ diff-lcs (~> 1.1.2)
76
+ rspec-mocks (2.7.0)
77
+ ruby_parser (2.3.1)
78
+ sexp_processor (~> 3.0)
79
+ sexp_processor (3.0.7)
80
+ slop (2.1.0)
81
+ spork (0.9.0.rc9)
82
+ sprockets (2.0.3)
83
+ hike (~> 1.2)
84
+ rack (~> 1.0)
85
+ tilt (~> 1.1, != 1.3.0)
86
+ systemu (2.4.1)
87
+ term-ansicolor (1.0.7)
88
+ thor (0.14.6)
89
+ tilt (1.3.3)
90
+ uuid (2.3.4)
91
+ macaddr (~> 1.0)
92
+
93
+ PLATFORMS
94
+ ruby
95
+
96
+ DEPENDENCIES
97
+ actionpack
98
+ foreman
99
+ growl_notify
100
+ guard
101
+ guard-rspec
102
+ guard-spork
103
+ pry
104
+ pry-remote
105
+ rake
106
+ rb-fsevent
107
+ rspec (>= 2.6.0)
108
+ secure_escrow!
109
+ spork (~> 0.9.0.rc9)
110
+ uuid
data/Guardfile ADDED
@@ -0,0 +1,33 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+ #
4
+ guard 'spork', :cucumber_env => { 'RAILS_ENV' => 'test' }, :rspec_env => { 'RAILS_ENV' => 'test' } do
5
+ watch('config/application.rb')
6
+ watch('config/environment.rb')
7
+ watch(%r{^config/environments/.+\.rb$})
8
+ watch(%r{^config/initializers/.+\.rb$})
9
+ watch('Gemfile')
10
+ watch('Gemfile.lock')
11
+ watch('spec/spec_helper.rb')
12
+ watch('test/test_helper.rb')
13
+ end
14
+
15
+ guard 'rspec', :version => 2 do
16
+ watch(%r{^spec/.+_spec\.rb$})
17
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
18
+ watch('spec/spec_helper.rb') { "spec" }
19
+ watch(%r{^spec/mock(_.+)\.rb$}) { "spec" }
20
+
21
+ # Rails example
22
+ watch(%r{^spec/.+_spec\.rb$})
23
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
24
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
25
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
26
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
27
+ watch('spec/spec_helper.rb') { "spec" }
28
+ watch('config/routes.rb') { "spec/routing" }
29
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
30
+ # Capybara request specs
31
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/requests/#{m[1]}_spec.rb" }
32
+ end
33
+
data/License ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2011 Duncan Beevers - Dweebd LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
8
+
data/Procfile ADDED
@@ -0,0 +1,3 @@
1
+ spork: bundle exec spork
2
+ redis: redis-server
3
+
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ task default: :spec
5
+
6
+ desc "Run specs"
7
+ RSpec::Core::RakeTask.new
8
+
data/Readme.md ADDED
@@ -0,0 +1,101 @@
1
+ # SecureEscrow
2
+ SecureEscrow proxies requests made to your application on a secure domain, stores the response, redirects the user to resources on an insecure domain, and serves the proxied response. It uses your preferred key-value store to hold responses between requests.
3
+
4
+ ## The Goals
5
+ This tool was created for two purposes:
6
+
7
+ - Secure authentication from an insecure page without a full-page refresh
8
+ - Secure authentication from a 3rd party-domain
9
+
10
+ ## The Solution
11
+ Your Rails application needs very little modification in order to support these secure actions.
12
+
13
+ Say you have a <tt>#signin</tt> ajax action that is currently insecure. Secure Escrow first helps you generate the same form with its action pointing to a secure domain. Also, instead of submitting the form via XHR, an iframe is dynamically-created and the form's target is set to that iframe. When the submission is received by the Secure Escrow middleware, the request is forwarded along to your Rails application. The response from Rails is then cached and an alternate response is delivered to the client. The client is redirected to GET action on the insecure domain. This action is served by the Secure Escrow middleware, and contains the cached response from the Rails application, leaving your app totally unaware that a redirection has occurred. The response is then parsed and passed back to your registered AJAX handler.
14
+
15
+ ## Installation
16
+ Add the following line to your <tt>Gemfile</tt>.
17
+
18
+ ````ruby
19
+ gem 'secure_escrow'
20
+ ````
21
+
22
+ Update your application's bundle.
23
+
24
+ ````
25
+ $ bundle install
26
+ ````
27
+
28
+ ## Usage
29
+ SecureEscrow has 5 integration points with a Rails application
30
+
31
+ - Middleware surrounding your application
32
+ - Domain information in application configuration
33
+ - Routes declared as escrowed
34
+ - JavaScript delivered via the asset pipeline
35
+ - Forms generated by views
36
+
37
+ ### Install as Middleware
38
+ Add the SecureEscrow::Middleware around your Rails application. It must be configured with access to a backing key-value store.
39
+ In this example, the <tt>Awesome::Application.config.redis</tt> value is available after the <tt>environment</tt> file has been run.
40
+
41
+ Example <tt>config.ru</tt>:
42
+
43
+ ````ruby
44
+ # This file is used by Rack-based servers to start the application.
45
+ require ::File.expand_path('../config/environment', __FILE__)
46
+ use SecureEscrow::Middleware, Awesome::Application.config.redis
47
+ run Awesome::Application
48
+ ````
49
+
50
+ ### Configure Domain Information
51
+ Ordinarily Rails simply uses the client-supplied request hostname to generate URLs. SecureEscrow needs to redirect to a
52
+ domain which may not be the same as the incoming request. Configuration is supplied on the Rails application instance, and
53
+ may differ for various environments. For example, in <tt>development.rb</tt> you might have the following config.
54
+
55
+ ````ruby
56
+ # = SecureEscrow config =
57
+ config.secure_domain_name = 'www.secure-awesome.devlocal'
58
+ config.secure_domain_protocol = 'http'
59
+ config.secure_domain_port = 3000
60
+ config.insecure_domain_name = 'www.insecure-awesome.devlocal'
61
+ config.insecure_domain_protocol = 'http'
62
+ config.insecure_domain_port = 3000
63
+ `````
64
+
65
+ ### Declare Escrowed Routes
66
+ In your Rails application's <tt>routes.rb</tt> file you can declare escrowed routes. Escrow only applies to POST actions.
67
+ In the following example, I show replacing a <tt>post</tt> route with an <tt>escrow</tt> route.
68
+
69
+ #### Before
70
+ ```` ruby
71
+ get 'signin' => 'sessions#new', as: :new_user_session
72
+ post 'signin' => 'sessions#create', as: :user_session
73
+ get 'signout'=> 'sessions#destroy', as: :destroy_user_session
74
+ ````
75
+
76
+ #### After
77
+ ````ruby
78
+ get 'signin' => 'sessions#new', as: :new_user_session
79
+ escrow 'signin' => 'sessions#create', as: :user_session
80
+ get 'signout'=> 'sessions#destroy', as: :destroy_user_session
81
+ ````
82
+
83
+ ### Deliver JavaScript assets
84
+ SecureEscrow integrates in the Rails asset pipeline. Just add the following to your <tt>application.js</tt>.
85
+
86
+ ````ruby
87
+ // SecureEscrow
88
+ // ======================
89
+ //= require secure_escrow
90
+ ````
91
+
92
+
93
+ ### Generate forms that submit to the escrow
94
+ View helpers are provided to generate forms that submit to the escrow.
95
+
96
+ Replace <tt>form_for</tt> with <tt>escrow_form_for</tt> and <tt>form_tag</tt> with <tt>escrow_form_tag</tt>.
97
+
98
+ ## License
99
+
100
+ MIT
101
+
@@ -0,0 +1,68 @@
1
+ (function($, undefined) {
2
+ var framesCount = 0;
3
+
4
+ function handleRemoteEscrowSubmission(form) {
5
+ framesCount++;
6
+ var name = 'escrow_frame_' + framesCount;
7
+
8
+ // Create a hidden iframe to submit a form to
9
+ var iframe = $('<iframe>', { name: name, id: name }).
10
+ css({ display: 'none' }).
11
+ appendTo('body');
12
+
13
+ // onLoad handler attached to hidden iframe
14
+ // Consumes iframe content and bubbles it up as an ajax response
15
+ function onLoad() {
16
+ var payload_error,
17
+ innerText, json, response;
18
+
19
+ try {
20
+ innerText = iframe[0].contentWindow.document.body.innerText;
21
+ try {
22
+ json = JSON.parse(innerText);
23
+ } catch(_2) {
24
+ payload_error = 'Error parsing JSON response';
25
+ }
26
+ } catch(_1) {
27
+ payload_error = 'Error accessing response body';
28
+ }
29
+
30
+
31
+ response = {
32
+ responseText: innerText
33
+ };
34
+
35
+ if (payload_error) {
36
+ response.errorJSON = { error: payload_error };
37
+ } else {
38
+ response.responseJSON = json;
39
+ }
40
+
41
+ if (json && json.success) {
42
+ form.trigger('ajax:success', response);
43
+ } else {
44
+ form.trigger('ajax:error', response);
45
+ }
46
+ form.trigger('ajax:complete', response);
47
+ }
48
+
49
+ iframe.load(onLoad);
50
+ form.attr('target', name);
51
+ }
52
+
53
+ var formSubmitSelector = 'form';
54
+
55
+ $(formSubmitSelector).live('submit.secure_escrow', function(event) {
56
+ var form = $(this),
57
+ escrow = form.data('escrow'),
58
+ isEscrow = escrow !== undefined;
59
+
60
+ if (isEscrow) {
61
+ if ('iframe' === escrow) {
62
+ handleRemoteEscrowSubmission(form);
63
+ }
64
+ }
65
+
66
+ });
67
+
68
+ }(jQuery));
@@ -0,0 +1,16 @@
1
+ require 'rails'
2
+
3
+ # Register as a Rails engine
4
+ # in order to hook into asset pipeline
5
+ module SecureEscrow
6
+ class Engine < ::Rails::Engine
7
+ initializer :setup_escrow do |app|
8
+ # Mix view helpers in through ActionController
9
+ ActionController::Base.helper SecureEscrow::Railtie::ActionViewHelper
10
+
11
+ # Mix routing helpers in through the Mapper class
12
+ ActionDispatch::Routing::Mapper.send :include, SecureEscrow::Railtie::Routing
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,244 @@
1
+ require "uuid"
2
+
3
+ module SecureEscrow
4
+ module MiddlewareConstants
5
+ REQUEST_METHOD = 'REQUEST_METHOD'
6
+ HTTP_COOKIE = 'HTTP_COOKIE'
7
+ REQUEST_PATH = 'REQUEST_PATH'
8
+ QUERY_STRING = 'QUERY_STRING'
9
+ POST = 'POST'
10
+ GET = 'GET'
11
+ COOKIE_SEPARATOR = ';'
12
+ RAILS_ROUTES = 'action_dispatch.routes'
13
+ LOCATION = 'Location'
14
+ ESCROW_MATCH = /^(.+)\.(.+)$/
15
+ TTL = 180 # Seconds until proxied response expires
16
+ NONCE = 'nonce'
17
+ RESPONSE = 'response'
18
+ BAD_NONCE = 'Bad nonce'
19
+ DATA_KEY = 'secure_escrow'
20
+ REDIRECT_CODES = 300..399
21
+ end
22
+
23
+ class Middleware
24
+ def initialize app, store
25
+ @app = app
26
+ @store = store
27
+ end
28
+
29
+ def call env
30
+ handle_presenter presenter(env)
31
+ end
32
+
33
+ def presenter env
34
+ Presenter.new @app, @store, env
35
+ end
36
+
37
+ def handle_presenter e
38
+ if e.serve_response_from_escrow?
39
+ e.serve_response_from_escrow!
40
+ elsif e.response_is_redirect?
41
+ e.redirect_to_response!
42
+ elsif e.store_response_in_escrow?
43
+ e.store_response_in_escrow_and_redirect!
44
+ else
45
+ e.serve_response_from_application!
46
+ end
47
+ end
48
+
49
+ class Presenter
50
+ include MiddlewareConstants
51
+
52
+ attr_reader :app, :store, :env
53
+
54
+ def initialize app, store, env
55
+ @app = app
56
+ @store = store
57
+ @env = env
58
+ end
59
+
60
+ def serve_response_from_escrow?
61
+ return false unless GET == env[REQUEST_METHOD]
62
+ return false unless escrow_id
63
+
64
+ store.exists escrow_key(escrow_id)
65
+ end
66
+
67
+ def response_is_redirect?
68
+ status, header, response = call_result
69
+ REDIRECT_CODES.include? status
70
+ end
71
+
72
+ def store_response_in_escrow?
73
+ method = env[REQUEST_METHOD]
74
+ return false unless POST == method
75
+ recognize_path[:escrow]
76
+ end
77
+
78
+ def serve_response_from_escrow!
79
+ key = escrow_key escrow_id
80
+ value = JSON.parse(store.get key)
81
+
82
+ if escrow_nonce == value[NONCE]
83
+ # Destroy the stored value
84
+ store.del key
85
+
86
+ return value[RESPONSE]
87
+ else
88
+ # HTTP Status Code 403 - Forbidden
89
+ return [ 403, {}, [ BAD_NONCE ] ]
90
+ end
91
+ end
92
+
93
+ def redirect_to_response!
94
+ status, header, response = call_result
95
+ rewrite_location_header! header
96
+ [ status, header, response ]
97
+ end
98
+
99
+ def store_response_in_escrow_and_redirect!
100
+ status, header, response = call_result
101
+ id, nonce = store_in_escrow status, header, response
102
+ token = "#{id}.#{nonce}"
103
+
104
+ response_headers = { LOCATION => redirect_to_location(token) }
105
+ set_cookie_token!(response_headers, token) if homogenous_host_names?
106
+
107
+ # HTTP Status Code 303 - See Other
108
+ return [ 303, response_headers, [ "Escrowed at #{token}" ] ]
109
+ end
110
+
111
+ def serve_response_from_application!
112
+ call_result
113
+ end
114
+
115
+ def escrow_id
116
+ @escrow_id ||= (escrow_id_and_nonce || [])[0]
117
+ end
118
+
119
+ def escrow_nonce
120
+ @escrow_nonce ||= (escrow_id_and_nonce || [])[1]
121
+ end
122
+
123
+ def escrow_key id
124
+ "escrow:#{id}"
125
+ end
126
+
127
+ # Take a Rack status, header, and response
128
+ # Serialize the response to a string
129
+ # Serialize the structure as JSON
130
+ # Generate a unique id for the data
131
+ # Generate a nonce for the data
132
+ # Store in Redis
133
+ def store_in_escrow status, header, response
134
+ id, nonce = generate_id_and_nonce
135
+
136
+ response_body = []
137
+ response.each { |content| response_body.push(content) }
138
+ response.close if response.respond_to? :close
139
+
140
+ rewrite_location_header! header
141
+
142
+ value = {
143
+ NONCE => nonce,
144
+ RESPONSE => [ status, header, [ response_body.join ] ]
145
+ }
146
+
147
+ # Serialze the nonce and Rack response triplet,
148
+ # store in Redis, and set TTL
149
+ key = escrow_key id
150
+ store.setex key, value.to_json, TTL
151
+
152
+ [ id, nonce ]
153
+ end
154
+
155
+ def generate_id_and_nonce
156
+ [ UUID.generate, SecureRandom.hex(4) ]
157
+ end
158
+
159
+ private
160
+ def set_cookie_token! headers, token
161
+ Rack::Utils.set_cookie_header!(headers, DATA_KEY,
162
+ value: token,
163
+ httponly: true)
164
+ end
165
+
166
+ def redirect_to_location token = nil
167
+ routes = app.routes
168
+ config = app.config
169
+
170
+ redirect_to_options = {
171
+ protocol: config.insecure_domain_protocol,
172
+ host: config.insecure_domain_name,
173
+ port: config.insecure_domain_port
174
+ }
175
+
176
+ if token && !homogenous_host_names?
177
+ redirect_to_options.merge!(DATA_KEY => token)
178
+ end
179
+
180
+ routes.url_for(
181
+ recognize_path.merge(redirect_to_options))
182
+ end
183
+
184
+ def rewrite_location_header! header
185
+ return unless header[LOCATION]
186
+
187
+ config = app.config
188
+ routes = app.routes
189
+
190
+ # Rewrite redirect to secure domain
191
+ header[LOCATION] = routes.url_for(
192
+ routes.recognize_path(header[LOCATION]).merge(
193
+ host: config.insecure_domain_name,
194
+ protocol: config.insecure_domain_protocol,
195
+ port: config.insecure_domain_port
196
+ ))
197
+
198
+ header
199
+ end
200
+
201
+ def call_result
202
+ @call_result ||= app.call env
203
+ end
204
+
205
+ def rails_routes
206
+ @rails_routes ||= app.routes
207
+ end
208
+
209
+ # TODO: Examine the performance implications of parsing the
210
+ # Cookie / Query payload this early in the stack
211
+ def escrow_id_and_nonce
212
+ data = (homogenous_host_names? ?
213
+ Rack::Utils.parse_query(env[HTTP_COOKIE], COOKIE_SEPARATOR) :
214
+ Rack::Utils.parse_query(env[QUERY_STRING]))[DATA_KEY]
215
+
216
+ return unless data
217
+ match = data.match ESCROW_MATCH
218
+ return unless match
219
+
220
+ match[1..2]
221
+ end
222
+
223
+ def homogenous_host_names?
224
+ config = app.config
225
+ config.secure_domain_name == config.insecure_domain_name
226
+ end
227
+
228
+ def recognize_path
229
+ begin
230
+ rails_routes.recognize_path(
231
+ env[REQUEST_PATH],
232
+ method: env[REQUEST_METHOD]
233
+ )
234
+ rescue ActionController::RoutingError
235
+ {}
236
+ end
237
+ end
238
+
239
+ end
240
+
241
+ end
242
+
243
+ end
244
+
@@ -0,0 +1,58 @@
1
+ require "action_dispatch"
2
+ require "action_pack"
3
+
4
+ module SecureEscrow
5
+ module Railtie
6
+ module Routing
7
+ def escrow options, &block
8
+ defaults = options[:defaults] || {}
9
+ defaults[:escrow] = true
10
+ post options.merge(defaults), &block
11
+ end
12
+ end
13
+
14
+ module ActionViewHelper
15
+ DATA_ESCROW = 'data-escrow'
16
+ IFRAME = 'iframe'
17
+ POST = 'post'
18
+
19
+ def escrow_form_for record, options = {}, &proc
20
+ options[:html] ||= {}
21
+
22
+ stringy_record = String === record || Symbol === record
23
+ apply_form_for_options!(record, options) unless stringy_record
24
+
25
+
26
+ form_for record, escrow_options(options), &proc
27
+ end
28
+
29
+ def escrow_form_tag url_for_options = {}, options = {}, &block
30
+ form_tag url_for_options, escrow_options(options), &block
31
+ end
32
+
33
+ private
34
+ def escrow_options options
35
+ # Rewrite URL to point to secure domain
36
+ app = Rails.application
37
+ config = app.config
38
+
39
+ submission_url = controller.url_for(
40
+ app.routes.recognize_path(options[:url]).
41
+ merge(
42
+ host: config.secure_domain_name,
43
+ protocol: config.secure_domain_protocol,
44
+ port: config.secure_domain_port
45
+ ))
46
+
47
+ options[:url] = submission_url
48
+
49
+ # Add data-escrow attribute to the form element
50
+ html_options = options[:html] || {}
51
+
52
+ escrow_method = options.delete(:remote) ? IFRAME : POST
53
+ options.merge(html: html_options.merge(DATA_ESCROW => escrow_method))
54
+ end
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,3 @@
1
+ module SecureEscrow
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,13 @@
1
+ # Attempt to provide engine to Rails
2
+ require "secure_escrow/engine"
3
+
4
+ require "secure_escrow/version"
5
+ require "secure_escrow/railtie"
6
+ require "secure_escrow/middleware"
7
+
8
+ module SecureEscrow
9
+ def self.included base
10
+ base.extend ClassMethods
11
+ end
12
+ end
13
+
@@ -0,0 +1,23 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "secure_escrow/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "secure_escrow"
7
+ s.version = SecureEscrow::VERSION
8
+ s.authors = ["Duncan Beevers"]
9
+ s.email = ["duncan@dweebd.com"]
10
+ s.homepage = ""
11
+ s.summary = "Secure AJAX-style actions for Rails applications"
12
+ s.description = "SecureEscrow provides a content proxy for Rails applications allowing POSTing to secure actions from insecure domains without full-page refreshes"
13
+
14
+ s.rubyforge_project = "secure_escrow"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency 'rspec'
22
+ end
23
+
@@ -0,0 +1,439 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+ include SecureEscrow::MiddlewareConstants
3
+
4
+ describe 'SecureEscrow::Middleware' do
5
+ let(:app) { MockEngine.new }
6
+ let(:store) { MockRedis.new }
7
+ let(:middleware) { SecureEscrow::Middleware.new app, store }
8
+ let(:presenter) { SecureEscrow::Middleware::Presenter.new app, store, env }
9
+ let(:env) { {} }
10
+
11
+ context 'as a Rack application' do
12
+ it 'should be callable' do
13
+ middleware.should respond_to(:call).with(1).argument
14
+ end
15
+
16
+ it 'should handle_presenter with wrapped environment' do
17
+ middleware.should_receive(:presenter).with(env).
18
+ once.and_return(presenter)
19
+
20
+ middleware.should_receive(:handle_presenter).with(presenter).once
21
+
22
+ middleware.call(env)
23
+ end
24
+
25
+ context 'when handling the presenter' do
26
+ it 'should first serve a response from escrow' do
27
+ presenter.should_receive(:serve_response_from_escrow?).
28
+ with.once.and_return(true)
29
+
30
+ presenter.should_receive(:serve_response_from_escrow!).
31
+ with.once
32
+
33
+ presenter.should_not_receive(:redirect_to_response!)
34
+ presenter.should_not_receive(:store_response_in_escrow_and_redirect!)
35
+ presenter.should_not_receive(:serve_response_from_application!)
36
+
37
+ middleware.handle_presenter presenter
38
+ end
39
+
40
+ it 'should use the response redirect' do
41
+ presenter.should_receive(:serve_response_from_escrow?).
42
+ once.and_return(false)
43
+ presenter.should_receive(:response_is_redirect?).
44
+ once.and_return(true)
45
+ presenter.should_receive(:redirect_to_response!).once
46
+
47
+ presenter.should_not_receive(:store_response_in_escrow_and_redirect!)
48
+ presenter.should_not_receive(:serve_response_from_application!)
49
+
50
+ middleware.handle_presenter presenter
51
+ end
52
+
53
+ it 'should store a response in the escrow and redirect' do
54
+ presenter.should_receive(:serve_response_from_escrow?).
55
+ once.and_return(false)
56
+ presenter.should_receive(:response_is_redirect?).
57
+ once.and_return(false)
58
+ presenter.should_receive(:store_response_in_escrow?).
59
+ once.and_return(true)
60
+ presenter.should_receive(:store_response_in_escrow_and_redirect!).
61
+ once
62
+
63
+ presenter.should_not_receive(:serve_response_from_escrow!)
64
+ presenter.should_not_receive(:redirect_to_response!)
65
+ presenter.should_not_receive(:serve_response_from_application!)
66
+
67
+ middleware.handle_presenter presenter
68
+ end
69
+
70
+ it 'should pass-through other requests' do
71
+ presenter.should_receive(:serve_response_from_escrow?).
72
+ once.and_return(false)
73
+ presenter.should_receive(:response_is_redirect?).
74
+ once.and_return(false)
75
+ presenter.should_receive(:store_response_in_escrow?).
76
+ once.and_return(false)
77
+ presenter.should_receive(:serve_response_from_application!).
78
+ once
79
+
80
+ presenter.should_not_receive(:serve_response_from_escrow!)
81
+ presenter.should_not_receive(:redirect_to_response!)
82
+ presenter.should_not_receive(:store_response_in_escrow_and_redirect!)
83
+
84
+ middleware.handle_presenter presenter
85
+ end
86
+ end
87
+ end
88
+
89
+ context 'Presenter' do
90
+ describe 'serve_response_from_escrow?' do
91
+ it 'should not serve POSTs' do
92
+ presenter.env[REQUEST_METHOD] = POST
93
+ presenter.serve_response_from_escrow?.should be_false
94
+ end
95
+
96
+ it 'should not serve responses where the escrow key is not in the store' do
97
+ presenter.env[REQUEST_METHOD] = GET
98
+ set_escrow_cookie presenter, 'id'
99
+
100
+ presenter.serve_response_from_escrow?.should be_false
101
+ end
102
+
103
+ it 'should not check the backing store when no escrow param is present' do
104
+ presenter.env[REQUEST_METHOD] = GET
105
+
106
+ store.should_not_receive(:exists)
107
+ presenter.serve_response_from_escrow?
108
+ end
109
+
110
+ it 'should serve responses where the escrow key is in the store' do
111
+ presenter.env[REQUEST_METHOD] = GET
112
+ store_in_escrow store, 'id'
113
+
114
+ set_escrow_cookie presenter, 'id'
115
+ presenter.serve_response_from_escrow?.should be_true
116
+ end
117
+ end
118
+
119
+ describe 'response_is_redirect?' do
120
+ it 'should not include status codes less than 300' do
121
+ app.should_receive(:call).
122
+ once.with(env).and_return([ 299, {}, [ '' ] ])
123
+
124
+ presenter.response_is_redirect?.should be_false
125
+ end
126
+
127
+ it 'should not include status codes greater than 399' do
128
+ app.should_receive(:call).
129
+ once.with(env).and_return([ 400, {}, [ '' ] ])
130
+
131
+ presenter.response_is_redirect?.should be_false
132
+ end
133
+
134
+ it 'should include 304' do
135
+ app.should_receive(:call).
136
+ once.with(env).and_return([ 304, {}, [ '' ] ])
137
+
138
+ presenter.response_is_redirect?.should be_true
139
+ end
140
+ end
141
+
142
+ describe 'store_response_in_escrow?' do
143
+ it 'should not store GETs' do
144
+ presenter.env[REQUEST_METHOD] = GET
145
+ presenter.store_response_in_escrow?.should be_false
146
+ end
147
+
148
+ it 'should not store non-existent routes' do
149
+ presenter.env[REQUEST_METHOD] = POST
150
+
151
+ app.routes.should_receive(:recognize_path).
152
+ once.with(env[REQUEST_PATH], { method: POST }).
153
+ and_raise(
154
+ ActionController::RoutingError.new("No route matches #{env[REQUEST_PATH]}")
155
+ )
156
+
157
+ presenter.store_response_in_escrow?.should be_false
158
+ end
159
+
160
+ it 'should not store non-escrow routes' do
161
+ presenter.env[REQUEST_METHOD] = POST
162
+
163
+ app.routes.should_receive(:recognize_path).
164
+ once.with(env[REQUEST_PATH], { method: POST }).
165
+ and_return(controller: 'session', action: 'create')
166
+
167
+ presenter.store_response_in_escrow?.should be_false
168
+ end
169
+
170
+ it 'should store escrow routes' do
171
+ presenter.env[REQUEST_METHOD] = POST
172
+
173
+ app.routes.should_receive(:recognize_path).
174
+ once.with(env[REQUEST_PATH], { method: POST }).
175
+ and_return(controller: 'session', action: 'create', escrow: true)
176
+
177
+ presenter.store_response_in_escrow?.should be_true
178
+ end
179
+ end
180
+
181
+ describe 'serve_response_from_escrow!' do
182
+ it 'should return 403 - Forbidden when nonce does not match' do
183
+ store_in_escrow store, 'id', 'good-nonce', []
184
+
185
+ set_escrow_cookie presenter, 'id', 'bad-nonce'
186
+ presenter.serve_response_from_escrow![0].should eq 403
187
+ end
188
+
189
+ it 'should delete the key from the backing store' do
190
+ store_in_escrow store, 'id'
191
+ set_escrow_cookie presenter, 'id'
192
+
193
+ store.should_receive(:del).
194
+ once.with(presenter.escrow_key('id'))
195
+
196
+ presenter.serve_response_from_escrow!
197
+ end
198
+
199
+ it 'should return the escrowed response' do
200
+ response = [ 200, {}, [ 'text' ] ]
201
+ store_in_escrow store, 'id', 'nonce', response
202
+ set_escrow_cookie presenter, 'id', 'nonce'
203
+
204
+ presenter.serve_response_from_escrow!.should eq response
205
+ end
206
+ end
207
+
208
+ describe 'redirect_to_response!' do
209
+ it 'should use status code from application' do
210
+ response = [ 315, {}, [ '' ] ]
211
+ app.should_receive(:call).
212
+ once.with(env).and_return(response)
213
+
214
+ presenter.redirect_to_response!.should eq response
215
+ end
216
+
217
+ it 'should rewrite location' do
218
+ original_location = "%s://%s:%s/path/" % [
219
+ app.config.secure_domain_protocol,
220
+ app.config.secure_domain_name,
221
+ app.config.secure_domain_port
222
+ ]
223
+ expected_location = "%s://%s:%s/path/" % [
224
+ app.config.insecure_domain_protocol,
225
+ app.config.insecure_domain_name,
226
+ app.config.insecure_domain_port
227
+ ]
228
+
229
+ original_response = [ 315, { LOCATION => original_location }, [ '' ] ]
230
+ app.should_receive(:call).
231
+ once.with(env).and_return(original_response)
232
+
233
+ app.routes.should_receive(:recognize_path).
234
+ once.with(original_location).
235
+ and_return(controller: 'sessions', action: 'create')
236
+ app.routes.should_receive(:url_for).
237
+ once.with(
238
+ controller: 'sessions',
239
+ action: 'create',
240
+ host: app.config.insecure_domain_name,
241
+ protocol: app.config.insecure_domain_protocol,
242
+ port: app.config.insecure_domain_port
243
+ ).and_return(expected_location)
244
+
245
+ presenter.redirect_to_response![1][LOCATION].should eq expected_location
246
+ end
247
+ end
248
+
249
+ describe 'store_response_in_escrow_and_redirect!' do
250
+ it 'should return 303 - See Other' do
251
+ presenter.stub!(:store_in_escrow).and_return([ 'id', 'nonce' ])
252
+ presenter.stub!(:redirect_to_location).and_return('/')
253
+
254
+ presenter.store_response_in_escrow_and_redirect![0].should eq 303
255
+ end
256
+
257
+ context 'when insecure_domain_name is different from secure_domain_name' do
258
+ let(:app) {
259
+ MockEngine.new(
260
+ secure_domain_name: 'www.ssl-example.com',
261
+ insecure_domain_name: 'www.example.com'
262
+ )
263
+ }
264
+ end
265
+ context 'when insecure_domain_name is the same as secure_domain_name' do
266
+ it 'should set cookie header with escrow token' do
267
+ presenter.stub!(:store_in_escrow).and_return([ 'id', 'nonce' ])
268
+ presenter.stub!(:redirect_to_location).and_return('/')
269
+
270
+ set_cookie_header = presenter.
271
+ store_response_in_escrow_and_redirect![1]['Set-Cookie']
272
+
273
+ cookies = Rack::Utils.parse_query(set_cookie_header)
274
+ cookies[SecureEscrow::MiddlewareConstants::DATA_KEY].should eq 'id.nonce'
275
+ end
276
+ end
277
+ end
278
+
279
+ describe 'serve_response_from_application!' do
280
+ it 'should serve response from application' do
281
+ response = [ 200, {}, [ '' ] ]
282
+ app.should_receive(:call).
283
+ once.with(env).and_return(response)
284
+ presenter.serve_response_from_application!.should eq response
285
+ end
286
+ end
287
+
288
+ describe 'generate_id_and_nonce' do
289
+ it 'should generate id with UUID and nonce with SecureRandom' do
290
+ UUID.should_receive(:generate).and_return('id')
291
+ SecureRandom.should_receive(:hex).once.with(4).and_return('nonce')
292
+ presenter.generate_id_and_nonce
293
+ end
294
+ end
295
+
296
+ describe 'store_in_escrow' do
297
+ it 'should return generated id and nonce' do
298
+ presenter.should_receive(:generate_id_and_nonce).
299
+ once.with.and_return([ 'id', 'nonce' ])
300
+
301
+ presenter.stub! :rewrite_location_header!
302
+
303
+ id, nonce = presenter.store_in_escrow(200, {}, [])
304
+ id.should eq 'id'
305
+ nonce.should eq 'nonce'
306
+ end
307
+
308
+ it 'should store serialized response and set expiration' do
309
+ presenter.should_receive(:generate_id_and_nonce).
310
+ once.and_return([ 'id', 'nonce'])
311
+
312
+ key = presenter.escrow_key 'id'
313
+ response = [ 200, {}, [ '' ] ]
314
+ expected_stored_value = {
315
+ NONCE => 'nonce',
316
+ RESPONSE => response
317
+ }.to_json
318
+
319
+ store.should_receive(:set).once.with(key, expected_stored_value)
320
+ store.should_receive(:expire).
321
+ once.with(key, SecureEscrow::MiddlewareConstants::TTL)
322
+
323
+ presenter.store_in_escrow(*response)
324
+ end
325
+
326
+ context 'when insecure_domain_name is different from secure_domain_name' do
327
+ it 'should not add Location header when none was present' do
328
+ presenter.should_receive(:generate_id_and_nonce).
329
+ once.with.and_return([ 'id', 'nonce' ])
330
+
331
+ presenter.store_in_escrow 200, {}, []
332
+ end
333
+
334
+ it 'should rewrite domain of redirect to secure domain' do
335
+ original_redirect_url = "%s://%s:%s" % [
336
+ app.config.secure_domain_protocol,
337
+ app.config.secure_domain_name,
338
+ app.config.secure_domain_port
339
+ ]
340
+ rewritten_redirect_url = 'boo'
341
+
342
+ # This is a fairly large area of interactivity
343
+ # with ActionDispatch::Routing::RouteSet
344
+ presenter.should_receive(:generate_id_and_nonce).
345
+ once.with.and_return([ 'id', 'nonce'])
346
+
347
+ app.routes.should_receive(:recognize_path).
348
+ once.with(original_redirect_url).
349
+ and_return(controller: 'sessions', action: 'create')
350
+
351
+ app.routes.should_receive(:url_for).
352
+ once.with(
353
+ controller: 'sessions', action: 'create',
354
+ host: app.config.insecure_domain_name,
355
+ protocol: app.config.insecure_domain_protocol,
356
+ port: app.config.insecure_domain_port
357
+ ).and_return(rewritten_redirect_url)
358
+
359
+ expected_stored_value = {
360
+ NONCE => 'nonce',
361
+ RESPONSE => [ 303, { LOCATION => rewritten_redirect_url }, [ '' ] ]
362
+ }.to_json
363
+
364
+ key = presenter.escrow_key 'id'
365
+ store.should_receive(:set).
366
+ once.with(key, expected_stored_value)
367
+
368
+ presenter.store_in_escrow(
369
+ 303,
370
+ { LOCATION => original_redirect_url },
371
+ [ '' ]
372
+ )
373
+ end
374
+ end
375
+ end
376
+
377
+ describe 'escrow_id and escrow_nonce' do
378
+ context 'when insecure_domain_name is different from secure_domain_name' do
379
+ let(:app) {
380
+ MockEngine.new(
381
+ secure_domain_name: 'www.ssl-example.com',
382
+ insecure_domain_name: 'www.example.com'
383
+ )
384
+ }
385
+
386
+ it 'should recognize escrow id and nonce from query string' do
387
+ set_escrow_query_string presenter, 'id', 'nonce'
388
+
389
+ presenter.escrow_id.should eq 'id'
390
+ presenter.escrow_nonce.should eq 'nonce'
391
+ end
392
+ end
393
+
394
+ context 'when insecure_domain_name is the same as secure_domain_name' do
395
+ let(:app) {
396
+ MockEngine.new(
397
+ secure_domain_name: 'www.example.com',
398
+ insecure_domain_name: 'www.example.com'
399
+ )
400
+ }
401
+
402
+ it 'should recognize escrow id and nonce from cookie' do
403
+ set_escrow_cookie presenter, 'id', 'nonce'
404
+
405
+ presenter.escrow_id.should eq 'id'
406
+ presenter.escrow_nonce.should eq 'nonce'
407
+ end
408
+ end
409
+
410
+ end
411
+ end
412
+
413
+ end
414
+
415
+ def set_escrow_query_string presenter, id = 'id', nonce = 'nonce'
416
+ set_escrow_env QUERY_STRING, presenter, id, nonce
417
+ end
418
+
419
+ def set_escrow_cookie presenter, id = 'id', nonce = 'nonce'
420
+ set_escrow_env HTTP_COOKIE, presenter, id, nonce
421
+ end
422
+
423
+ def set_escrow_env key, presenter, id, nonce
424
+ presenter.env[key] = "%s=%s.%s" % [
425
+ SecureEscrow::MiddlewareConstants::DATA_KEY,
426
+ id, nonce
427
+ ]
428
+ end
429
+
430
+ def store_in_escrow store, id = 'id', nonce = 'nonce', response = []
431
+ store.set(
432
+ presenter.escrow_key(id),
433
+ ActiveSupport::JSON.encode(
434
+ NONCE => nonce,
435
+ RESPONSE => response
436
+ )
437
+ )
438
+ end
439
+
@@ -0,0 +1,37 @@
1
+ require 'ostruct'
2
+
3
+ class MockEngine
4
+ SUCCESS = 200
5
+
6
+ def initialize extra_config = {}
7
+ config extra_config
8
+ end
9
+
10
+ def call env
11
+ [ SUCCESS, {}, [ 'nada' ] ]
12
+ end
13
+
14
+ def config extra_config = {}
15
+ @config ||= Config.new(
16
+ {
17
+ secure_domain_name: 'www.example.com',
18
+ secure_domain_protocol: 'https',
19
+ secure_domain_port: 443,
20
+ insecure_domain_name: 'www.example.com',
21
+ insecure_domain_protocol: 'http',
22
+ insecure_domain_port: 80
23
+ }.merge extra_config
24
+ )
25
+ end
26
+
27
+ def routes
28
+ @routes ||= Routes.new
29
+ end
30
+
31
+ class Config < OpenStruct
32
+ end
33
+
34
+ class Routes
35
+ end
36
+ end
37
+
@@ -0,0 +1,31 @@
1
+ class MockRedis
2
+ def setex key, value, ttl
3
+ set key, value
4
+ expire key, ttl
5
+ end
6
+
7
+ def set key, value
8
+ values[key] = value
9
+ end
10
+
11
+ def get key
12
+ values[key]
13
+ end
14
+
15
+ def exists key
16
+ values.has_key? key
17
+ end
18
+
19
+ def del key
20
+ values.delete key
21
+ end
22
+
23
+ def expire key, ttl
24
+ end
25
+
26
+ private
27
+ def values
28
+ @values ||= {}
29
+ end
30
+ end
31
+
@@ -0,0 +1,20 @@
1
+ require 'spork'
2
+
3
+ Spork.prefork do
4
+ require 'rspec'
5
+ require 'pry-remote'
6
+ require 'active_support/json'
7
+ require 'action_controller'
8
+
9
+ RSpec.configure do |config|
10
+ end
11
+ end
12
+
13
+ Spork.each_run do
14
+ require 'secure_escrow'
15
+
16
+ # Load mocks
17
+ require 'mock_engine'
18
+ require 'mock_redis'
19
+ end
20
+
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: secure_escrow
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Duncan Beevers
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rspec
16
+ requirement: &70248523590320 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *70248523590320
25
+ description: SecureEscrow provides a content proxy for Rails applications allowing
26
+ POSTing to secure actions from insecure domains without full-page refreshes
27
+ email:
28
+ - duncan@dweebd.com
29
+ executables: []
30
+ extensions: []
31
+ extra_rdoc_files: []
32
+ files:
33
+ - .gitignore
34
+ - Gemfile
35
+ - Gemfile.lock
36
+ - Guardfile
37
+ - License
38
+ - Procfile
39
+ - Rakefile
40
+ - Readme.md
41
+ - lib/assets/javascripts/secure_escrow.js
42
+ - lib/secure_escrow.rb
43
+ - lib/secure_escrow/engine.rb
44
+ - lib/secure_escrow/middleware.rb
45
+ - lib/secure_escrow/railtie.rb
46
+ - lib/secure_escrow/version.rb
47
+ - secure_escrow.gemspec
48
+ - spec/middleware_spec.rb
49
+ - spec/mock_engine.rb
50
+ - spec/mock_redis.rb
51
+ - spec/spec_helper.rb
52
+ homepage: ''
53
+ licenses: []
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ required_rubygems_version: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ requirements: []
71
+ rubyforge_project: secure_escrow
72
+ rubygems_version: 1.8.10
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Secure AJAX-style actions for Rails applications
76
+ test_files:
77
+ - spec/middleware_spec.rb
78
+ - spec/mock_engine.rb
79
+ - spec/mock_redis.rb
80
+ - spec/spec_helper.rb