session_injector 0.0.1.snapshot

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module SessionInjector
3
+ VERSION = "0.0.1.snapshot"
4
+ end
5
+ end
@@ -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
+