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.
@@ -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
+