session_injector 0.0.1.snapshot
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/session_injector/version.rb +5 -0
- data/lib/session_injector.rb +171 -0
- metadata +100 -0
@@ -0,0 +1,171 @@
|
|
1
|
+
require 'active_support/message_encryptor'
|
2
|
+
require 'uri'
|
3
|
+
|
4
|
+
module Rack
|
5
|
+
module Middleware
|
6
|
+
class SessionInjector
|
7
|
+
|
8
|
+
class InvalidHandshake < StandardError; end
|
9
|
+
|
10
|
+
DEFAULT_OPTIONS = {
|
11
|
+
# use the AbstractStore default key as our session id key
|
12
|
+
# if you have configured a custom session store key, you must
|
13
|
+
# specify that as the value for this middleware
|
14
|
+
:key => ActionDispatch::Session::AbstractStore::DEFAULT_OPTIONS[:key],
|
15
|
+
:token_lifetime => 5000, # five seconds should be enough
|
16
|
+
:die_on_handshake_failure => true
|
17
|
+
}
|
18
|
+
|
19
|
+
# the env key we will use to stash ourselves for downstream access
|
20
|
+
SESSION_INJECTOR_KEY = '_session_injector';
|
21
|
+
# the env key upstream uses to stash a flag to tell us to propagate a session
|
22
|
+
# this is a convenience for manually adding a request parameter to a redirect response location
|
23
|
+
SESSION_PROPAGATE_KEY = '_session_propagate';
|
24
|
+
|
25
|
+
# the internal parameter we will use to convey the session handshake token
|
26
|
+
HANDSHAKE_PARAM = '_hs_';
|
27
|
+
|
28
|
+
def initialize(app, options = {})
|
29
|
+
@app = app
|
30
|
+
options = DEFAULT_OPTIONS.merge(options)
|
31
|
+
@session_id_key = options[:key]
|
32
|
+
# statically generated token key in case we
|
33
|
+
# need to fall back (no cookie token key has been set)
|
34
|
+
# handshakes are by definition transient, so the only
|
35
|
+
# important requirement is that the middleware that generates
|
36
|
+
# the token can decrypt the token. when not under a clustered/balanced
|
37
|
+
# architecture, that most likely means the same process/middelware
|
38
|
+
# so the key value is not important
|
39
|
+
# in fact, non-durability of the token is a security feature
|
40
|
+
generated_token_key = ActiveSupport::SecureRandom.hex(16)
|
41
|
+
@token_key = options[:token_key] || generated_token_key
|
42
|
+
@enforced_lifetime = options[:token_lifetime]
|
43
|
+
@die_on_handshake_failure = options[:die_on_handshake_failure]
|
44
|
+
end
|
45
|
+
|
46
|
+
def call(env)
|
47
|
+
env[SESSION_INJECTOR_KEY] = self; # store ourselves for downstream access
|
48
|
+
reconstitute_session(env)
|
49
|
+
response = @app.call(env)
|
50
|
+
response = propagate_session(env, *response)
|
51
|
+
response
|
52
|
+
end
|
53
|
+
|
54
|
+
# rewrites location header if requested
|
55
|
+
def propagate_session(env, status, headers, response)
|
56
|
+
propagate_flag = env.delete(SESSION_PROPAGATE_KEY)
|
57
|
+
location = headers["Location"]
|
58
|
+
if propagate_flag and location
|
59
|
+
# we've been told to rewrite the location header and it is present
|
60
|
+
uri = URI::parse(location)
|
61
|
+
prefix = uri.query ? "&" : ""
|
62
|
+
# append handshake param to query
|
63
|
+
uri.query = [uri.query, prefix, SessionInjector.generate_handshake_parameter(Rack::Request.new(env), propagate_flag[0], propagate_flag[1])].join
|
64
|
+
headers["Location"] = uri.to_s
|
65
|
+
end
|
66
|
+
[ status, headers, response]
|
67
|
+
end
|
68
|
+
|
69
|
+
# generates the handshake token we can send to the target domain
|
70
|
+
def self.generate_handshake_token(request, target_domain, lifetime = nil)
|
71
|
+
# retrieve the configured middleware instance
|
72
|
+
session_injector = request.env[SESSION_INJECTOR_KEY]
|
73
|
+
# note: scheme is not included in handshake
|
74
|
+
# a session initiated on https may be established on http
|
75
|
+
handshake = {
|
76
|
+
:request_ip => request.ip,
|
77
|
+
:request_path => request.fullpath, # more for accounting/stats than anything else
|
78
|
+
:src_domain => request.host,
|
79
|
+
:tgt_domain => target_domain,
|
80
|
+
:token_create_time => Time.now.to_i,
|
81
|
+
# the most important thing
|
82
|
+
:session_id => extract_session_id(request, session_injector.session_id_key)
|
83
|
+
}
|
84
|
+
handshake[:requested_lifetime] = lifetime if lifetime
|
85
|
+
# we could reuse ActionDispatch::Cookies.TOKEN_KEY if it is present but let's not!
|
86
|
+
ActiveSupport::MessageEncryptor.new(session_injector.token_key).encrypt_and_sign(handshake);
|
87
|
+
end
|
88
|
+
|
89
|
+
# generates the handshake parameter key=value string
|
90
|
+
def self.generate_handshake_parameter(request, target_domain, lifetime = nil)
|
91
|
+
"#{HANDSHAKE_PARAM}=#{generate_handshake_token(request, target_domain, lifetime)}"
|
92
|
+
end
|
93
|
+
|
94
|
+
# helper that sets a flag to rewrite the location header with session propagation handshake
|
95
|
+
def self.propagate_session(request, target_domain, lifetime = nil)
|
96
|
+
request.env[SESSION_PROPAGATE_KEY] = [ target_domain, lifetime ]
|
97
|
+
end
|
98
|
+
|
99
|
+
# find the current session id
|
100
|
+
def self.extract_session_id(request, session_id_key)
|
101
|
+
#request.session_options[:id]
|
102
|
+
request.cookies[session_id_key]
|
103
|
+
end
|
104
|
+
|
105
|
+
# return the env key containing the session id
|
106
|
+
def session_id_key
|
107
|
+
@session_id_key
|
108
|
+
end
|
109
|
+
|
110
|
+
# return the key we use for encryption and hashing
|
111
|
+
def token_key
|
112
|
+
@token_key
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
# validates the handshake against the current environment
|
118
|
+
def validate_handshake(handshake, env)
|
119
|
+
# is the handshake token expired?
|
120
|
+
token_create_time = handshake[:token_create_time]
|
121
|
+
raise InvalidHandshake, "token creation time missing" unless token_create_time
|
122
|
+
now = Time.now.to_i
|
123
|
+
token_age = now - token_create_time
|
124
|
+
raise InvalidHandshake, "token has is expired" unless token_age < @enforced_lifetime
|
125
|
+
# ok, we can accept this token, but does the source want us to?
|
126
|
+
raise InvalidHandshake, "token has outlived requested expiration" if handshake[:requested_lifetime] and token_age > handshake[:requested_lifetime]
|
127
|
+
|
128
|
+
# cool, token is not expired
|
129
|
+
# is it for the right domain?
|
130
|
+
this_request = Rack::Request.new(env)
|
131
|
+
raise InvalidHandshake, "target domain mismatch" unless handshake[:tgt_domain] == this_request.host
|
132
|
+
|
133
|
+
# it's FOR the right domain
|
134
|
+
# is it FROM the right domain?
|
135
|
+
# SKIP THIS CHECK
|
136
|
+
# 'referrer' is not reliable, is up to the client to send, and we may not always be coming from a redirect
|
137
|
+
# raise InvalidHandshake, "source domain mismatch" unless handshake[:src_domain] == URI::parse(this_request.referrer).host
|
138
|
+
|
139
|
+
# finally, is this the same client that was associated with the source session?
|
140
|
+
# this really should be the case unless some shenanigans is going on (either somebody is replaying the token
|
141
|
+
# or there is some client balancing or proxying going on)
|
142
|
+
raise InvalidHandshake, "client ip mismatch" unless handshake[:request_ip] = this_request.ip
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
# load and inject any session that might be conveyed in this request
|
148
|
+
def reconstitute_session(env)
|
149
|
+
request = Rack::Request.new(env)
|
150
|
+
token = request.params[HANDSHAKE_PARAM]
|
151
|
+
return unless token
|
152
|
+
# decrypt the token and set the session id
|
153
|
+
handshake = decrypt_handshake_token(token, env)
|
154
|
+
#env[@session_id_key] = handshake[:session_id] if handshake
|
155
|
+
request.cookies[@session_id_key] = handshake[:session_id]
|
156
|
+
end
|
157
|
+
|
158
|
+
# decrypts a handshake token sent to us from a source domain
|
159
|
+
def decrypt_handshake_token(token, env)
|
160
|
+
handshake = ActiveSupport::MessageEncryptor.new(@token_key).decrypt_and_verify(token);
|
161
|
+
begin
|
162
|
+
validate_handshake(handshake, env)
|
163
|
+
return handshake
|
164
|
+
rescue InvalidHandshake
|
165
|
+
raise if @die_on_handshake_failure
|
166
|
+
end
|
167
|
+
return nil
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: session_injector
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: -2083286358
|
5
|
+
prerelease: true
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
- snapshot
|
11
|
+
version: 0.0.1.snapshot
|
12
|
+
platform: ruby
|
13
|
+
authors:
|
14
|
+
- Aaron Hamid
|
15
|
+
autorequire:
|
16
|
+
bindir: bin
|
17
|
+
cert_chain: []
|
18
|
+
|
19
|
+
date: 2011-04-02 00:00:00 -04:00
|
20
|
+
default_executable:
|
21
|
+
dependencies:
|
22
|
+
- !ruby/object:Gem::Dependency
|
23
|
+
name: activesupport
|
24
|
+
prerelease: false
|
25
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
hash: 5
|
31
|
+
segments:
|
32
|
+
- 3
|
33
|
+
version: "3"
|
34
|
+
type: :runtime
|
35
|
+
version_requirements: *id001
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: rack
|
38
|
+
prerelease: false
|
39
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
hash: 11
|
45
|
+
segments:
|
46
|
+
- 1
|
47
|
+
- 2
|
48
|
+
version: "1.2"
|
49
|
+
type: :runtime
|
50
|
+
version_requirements: *id002
|
51
|
+
description: A Rack middleware that allows injecting a session across domains
|
52
|
+
email:
|
53
|
+
- aaron@incandescentsoftware.com
|
54
|
+
executables: []
|
55
|
+
|
56
|
+
extensions: []
|
57
|
+
|
58
|
+
extra_rdoc_files: []
|
59
|
+
|
60
|
+
files:
|
61
|
+
- lib/session_injector/version.rb
|
62
|
+
- lib/session_injector.rb
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: http://github.com/incandescent/session-injector
|
65
|
+
licenses: []
|
66
|
+
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ">="
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
hash: 3
|
78
|
+
segments:
|
79
|
+
- 0
|
80
|
+
version: "0"
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
|
+
none: false
|
83
|
+
requirements:
|
84
|
+
- - ">="
|
85
|
+
- !ruby/object:Gem::Version
|
86
|
+
hash: 23
|
87
|
+
segments:
|
88
|
+
- 1
|
89
|
+
- 3
|
90
|
+
- 6
|
91
|
+
version: 1.3.6
|
92
|
+
requirements: []
|
93
|
+
|
94
|
+
rubyforge_project:
|
95
|
+
rubygems_version: 1.3.7
|
96
|
+
signing_key:
|
97
|
+
specification_version: 3
|
98
|
+
summary: A Rack session injector middleware
|
99
|
+
test_files: []
|
100
|
+
|