secure_escrow 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +21 -0
- data/Gemfile.lock +110 -0
- data/Guardfile +33 -0
- data/License +8 -0
- data/Procfile +3 -0
- data/Rakefile +8 -0
- data/Readme.md +101 -0
- data/lib/assets/javascripts/secure_escrow.js +68 -0
- data/lib/secure_escrow/engine.rb +16 -0
- data/lib/secure_escrow/middleware.rb +244 -0
- data/lib/secure_escrow/railtie.rb +58 -0
- data/lib/secure_escrow/version.rb +3 -0
- data/lib/secure_escrow.rb +13 -0
- data/secure_escrow.gemspec +23 -0
- data/spec/middleware_spec.rb +439 -0
- data/spec/mock_engine.rb +37 -0
- data/spec/mock_redis.rb +31 -0
- data/spec/spec_helper.rb +20 -0
- metadata +80 -0
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
data/Rakefile
ADDED
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,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
|
+
|
data/spec/mock_engine.rb
ADDED
@@ -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
|
+
|
data/spec/mock_redis.rb
ADDED
@@ -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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|