has_global_session 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README +151 -0
- data/has_global_session.gemspec +32 -0
- data/init.rb +4 -0
- data/lib/generators/global_session_config/USAGE +2 -0
- data/lib/generators/global_session_config/global_session_config_generator.rb +19 -0
- data/lib/generators/global_session_config/templates/global_session.yml.erb +30 -0
- data/lib/has_global_session/configuration.rb +34 -0
- data/lib/has_global_session/directory.rb +30 -0
- data/lib/has_global_session/global_session.rb +152 -0
- data/lib/has_global_session/integrated_session.rb +41 -0
- data/lib/has_global_session.rb +10 -0
- data/rails/action_controller_instance_methods.rb +48 -0
- data/rails/init.rb +18 -0
- metadata +77 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
= Introduction
|
2
|
+
|
3
|
+
HasGlobalSession enables multiple heterogeneous Web applications to share
|
4
|
+
session state in a cryptographically secure way, facilitating single sign-on
|
5
|
+
and enabling easier development of large-scale distributed applications that
|
6
|
+
make use of architectural strategies such as sharding or separation of concerns.
|
7
|
+
|
8
|
+
This plugin does not provide a complete solution for authentication. In
|
9
|
+
particular, it does *not* provide any of the following:
|
10
|
+
|
11
|
+
* <b>federation</b> -- aka cross-domain single sign-on -- use OpenID for that.
|
12
|
+
|
13
|
+
* <b>authentication</b> -- the application's controllers must authenticate the user.
|
14
|
+
|
15
|
+
* <b>secrecy</b> -- global session attributes can be signed but never encrypted;
|
16
|
+
protect against third-party snooping using SSL, and if you don't want your
|
17
|
+
users to see their session state, put it in a database.
|
18
|
+
|
19
|
+
* <b>replication</b> -- the authentication authorities must have some way to
|
20
|
+
share information about the database of users, their passwords, etc.
|
21
|
+
|
22
|
+
* <b>single sign-out</b> -- the authorities must have some way to broadcast a
|
23
|
+
notification when sessions are invalidated; they can override the default
|
24
|
+
Directory implementation to do realtime revocation checking.
|
25
|
+
|
26
|
+
== Global Session Contents
|
27
|
+
|
28
|
+
Global session state is stored as a cookie in the user's browser. The cookie
|
29
|
+
is a Zlib-compressed JSON dictionary containing the following stuff:
|
30
|
+
* session metadata (UUID, created-at, expires-at, certifying-authority)
|
31
|
+
* signed session attributes (e.g. the authenticated user ID)
|
32
|
+
* insecure session attributes (e.g. the last-visited URL)
|
33
|
+
* a cryptographic signature of the metadata and signed attributes
|
34
|
+
|
35
|
+
The global session is unserialized and its signature is verified whenever
|
36
|
+
a controller asks for the global session. The cookie's value is updated
|
37
|
+
whenever its attributes change. As an optimization, the signature is only
|
38
|
+
recomputed when the metadata or signed attributes have changed; insecure
|
39
|
+
attributes can change "for free."
|
40
|
+
|
41
|
+
Because the security properties of attributes can vary, HasGlobalSession
|
42
|
+
requires all _possible_ attributes to be declared up-front in the config
|
43
|
+
file. The 'attributes' section of the config file defines the _schema_
|
44
|
+
for the global session: which attributes can be used, and which must be
|
45
|
+
cryptographically signed in order to be used.
|
46
|
+
|
47
|
+
Since the session is serialized as JSON, only a limited range of object
|
48
|
+
types can be stored in it: strings, numbers, lists, hashes and other Ruby
|
49
|
+
primitives. Ruby booleans (true/false) do not translate well into JSON
|
50
|
+
and should be avoided.
|
51
|
+
|
52
|
+
= Example
|
53
|
+
|
54
|
+
1) Create a basic config file and edit it to suit your needs:
|
55
|
+
$ script/generate global_session_config mycoolapp.com
|
56
|
+
|
57
|
+
2) Create an authentication authority:
|
58
|
+
$ script/generate global_session_authority mycoolapp
|
59
|
+
|
60
|
+
3) Declare that some or all of your application's controllers will have access
|
61
|
+
to the global session:
|
62
|
+
class ApplicationController < ActionController::Base
|
63
|
+
has_global_session
|
64
|
+
end
|
65
|
+
|
66
|
+
4) Make use of the global session hash in your controllers:
|
67
|
+
global_session['user'] = @user.id
|
68
|
+
...
|
69
|
+
@current_user = User.find(global_session['user'])
|
70
|
+
|
71
|
+
= Detailed Information
|
72
|
+
|
73
|
+
== Global Session Domain
|
74
|
+
|
75
|
+
We refer to collection of _all_ Web application instances capable of using the
|
76
|
+
global session as the "domain." The global session domain may consist of any
|
77
|
+
number of distinct servers, possibly hidden behind load balancers or proxies.
|
78
|
+
The servers within the domain may all be running the same Rails application,
|
79
|
+
or they may be running different codebases that represent different parts of
|
80
|
+
a distributed application. (They may also be using app frameworks other than
|
81
|
+
Rails.)
|
82
|
+
|
83
|
+
The only constraint imposed by HasGlobalSession is that all servers within the
|
84
|
+
domain must have end-user-facing URLs within the same second-level DNS domain.
|
85
|
+
This is due to limitations imposed by the HTTP cookie mechanism: for privacy
|
86
|
+
reasons, cookies will only be sent to servers within the same domain as the
|
87
|
+
server that first created them.
|
88
|
+
|
89
|
+
For example, in my HasGlobalSession configuration file I might specify that my
|
90
|
+
cookie's domain is "example.com". My app servers at app1.example.com and
|
91
|
+
app2.example.com would be part of the global session domain, but my business
|
92
|
+
partner's application at www.partner.com could not participate.
|
93
|
+
|
94
|
+
== Authorities and Relying Parties
|
95
|
+
|
96
|
+
A Web application that can create or update the global session is said to
|
97
|
+
be an "authority" (because it's trusted by other parties to make assertions
|
98
|
+
about global session state). An application that can read the global session
|
99
|
+
is said to be a "relying party." In practice, every application is a relying
|
100
|
+
party but not all of them need to be authorities.
|
101
|
+
|
102
|
+
There is an RSA key pair associated with each authority. The authority's
|
103
|
+
public key is distribued to all relying parties, but the private key must
|
104
|
+
remain a secret to that authority (which may consist of many individual
|
105
|
+
servers).
|
106
|
+
|
107
|
+
This system allows for significant flexibility when configuring a distributed
|
108
|
+
app's global session. There must be at least one authority, but for many apps
|
109
|
+
one authority (plus an arbitrary number of relying parties, which do not need
|
110
|
+
a key pair) will be sufficient.
|
111
|
+
|
112
|
+
In general, two systems should be part of the same authority if there is no
|
113
|
+
trust boundary between them -- that is to say, trust between the two systems
|
114
|
+
is unlimited in both directions.
|
115
|
+
|
116
|
+
Here are some reasons you might consider dividing your systems into different
|
117
|
+
authorities:
|
118
|
+
* beta/staging system vs. production system
|
119
|
+
* system hosted by a third party vs. system hosted internally
|
120
|
+
* e-commerce server vs. storefront server
|
121
|
+
|
122
|
+
== The Directory
|
123
|
+
|
124
|
+
The Directory is a Ruby object instantiated by HasGlobalSession in order to
|
125
|
+
perform lookups of public and private keys. Given an authority name (as found
|
126
|
+
in a session cookie), the Directory can find the corresponding public key.
|
127
|
+
|
128
|
+
If the local system is an authority itself, the method #my_authority_name will
|
129
|
+
return non-nil and #my_private_key will return a private key suitable for
|
130
|
+
signing session attributes.
|
131
|
+
|
132
|
+
The Directory implementation included with HasGlobalSession uses the filesystem
|
133
|
+
as the backing store for its key pairs. Its #initialize method accepts a
|
134
|
+
filesystem path that will be searched for *.pub and *.key files containing PEM-
|
135
|
+
encoded public and private keys (the same format used by OpenSSH).
|
136
|
+
|
137
|
+
When used with a Rails app, HasGlobalSession expects to find its keystore in
|
138
|
+
config/authorities. You can use the global_session generator to create new key
|
139
|
+
pairs. Remember never to check a *.key file into a public repository!! (In
|
140
|
+
contrast, *.pub files can be distributed freely.)
|
141
|
+
|
142
|
+
If you wish all of the systems to stop trusting an authority, simply delete
|
143
|
+
its public key from config/authorities.
|
144
|
+
|
145
|
+
= To-Do
|
146
|
+
|
147
|
+
* Configurable session expiry
|
148
|
+
* Option to auto-renew session
|
149
|
+
* Option for non-sticky global session (cookie expires at close of browser session, not at global session expiration!)
|
150
|
+
|
151
|
+
Copyright (c) 2010 Tony Spataro <code@tracker.xeger.net>, released under the MIT license
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
|
5
|
+
spec = Gem::Specification.new do |s|
|
6
|
+
s.required_rubygems_version = nil if s.respond_to? :required_rubygems_version=
|
7
|
+
s.required_ruby_version = Gem::Requirement.new(">= 1.8.7")
|
8
|
+
|
9
|
+
s.name = 'has_global_session'
|
10
|
+
s.version = '0.8.0'
|
11
|
+
s.date = '2010-06-01'
|
12
|
+
|
13
|
+
s.authors = ['Tony Spataro']
|
14
|
+
s.email = 'code@tracker.xeger.net'
|
15
|
+
s.homepage= 'http://github.com/xeger/has_global_session'
|
16
|
+
|
17
|
+
s.summary = %q{Secure single-domain session sharing plugin for Rails.}
|
18
|
+
s.description = %q{This Rails plugin allows several Rails web apps that share the same back-end user database to share session state in a cryptographically secure way, facilitating single sign-on in a distributed web app. It only provides session sharing and does not concern itself with authentication or replication of the user database.}
|
19
|
+
|
20
|
+
s.add_runtime_dependency('uuidtools', [">= 2.1.1"])
|
21
|
+
|
22
|
+
basedir = File.dirname(__FILE__)
|
23
|
+
candidates = ['has_global_session.gemspec', 'init.rb', 'MIT-LICENSE', 'README'] +
|
24
|
+
Dir['lib/**/*'] +
|
25
|
+
Dir['rails/**/*']
|
26
|
+
s.files = candidates.sort
|
27
|
+
end
|
28
|
+
|
29
|
+
if $PROGRAM_NAME == __FILE__
|
30
|
+
Gem.manage_gems if Gem::RubyGemsVersion.to_f < 1.0
|
31
|
+
Gem::Builder.new(spec).build
|
32
|
+
end
|
data/init.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
class GlobalSessionConfigGenerator < Rails::Generator::Base
|
2
|
+
def initialize(runtime_args, runtime_options = {})
|
3
|
+
super
|
4
|
+
|
5
|
+
@app_name = File.basename(RAILS_ROOT)
|
6
|
+
@app_domain = args.shift
|
7
|
+
raise ArgumentError, "Must specify DNS domain for global session cookie, e.g. 'example.com'" unless @app_domain
|
8
|
+
end
|
9
|
+
|
10
|
+
def manifest
|
11
|
+
record do |m|
|
12
|
+
|
13
|
+
m.template 'templates/global_session.yml.erb',
|
14
|
+
'config/global_session.yml',
|
15
|
+
:assigns=>{:app_name=>@app_name,
|
16
|
+
:app_domain=>@app_domain}
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Common settings of the global session (that apply to all Rails environments)
|
2
|
+
# are listed here. These may be overidden in the environment-specific section,
|
3
|
+
# but it seldom makes sense to do so.
|
4
|
+
common:
|
5
|
+
attributes:
|
6
|
+
# Signed attributes of the global session
|
7
|
+
signed:
|
8
|
+
- user
|
9
|
+
# Untrusted attributes of the global session
|
10
|
+
insecure:
|
11
|
+
- account
|
12
|
+
# Enable local session integration in order to use the ActionController
|
13
|
+
# method #session to access both local AND global session state, with
|
14
|
+
# global attributes always taking precedence over local attributes.
|
15
|
+
integrated: true
|
16
|
+
|
17
|
+
development:
|
18
|
+
cookie:
|
19
|
+
name: __<%= app_name %>_development
|
20
|
+
domain: localhost
|
21
|
+
|
22
|
+
test:
|
23
|
+
cookie:
|
24
|
+
name: __<%= app_name %>_test
|
25
|
+
domain: localhost
|
26
|
+
|
27
|
+
production:
|
28
|
+
cookie:
|
29
|
+
name: __<%= app_name %>_global
|
30
|
+
domain: <%= app_domain %>
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
module Configuration
|
3
|
+
mattr_accessor :config_file
|
4
|
+
mattr_accessor :environment
|
5
|
+
|
6
|
+
def self.[](key)
|
7
|
+
unless @config
|
8
|
+
raise MissingConfiguration, "config_file is nil; cannot read configuration" unless config_file
|
9
|
+
raise MissingConfiguration, "environment is nil; must be specified" unless environment
|
10
|
+
@config = YAML.load(File.read(config_file))
|
11
|
+
validate
|
12
|
+
end
|
13
|
+
if @config.has_key?(environment) && @config[environment].has_key?(key)
|
14
|
+
return @config[environment][key]
|
15
|
+
else
|
16
|
+
@config['common'][key]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.validate
|
21
|
+
['attributes/signed', 'integrated', 'cookie/name', 'cookie/domain'].each do |path|
|
22
|
+
elements = path.split '/'
|
23
|
+
object = self[elements.shift]
|
24
|
+
elements.each do |element|
|
25
|
+
object = object[element]
|
26
|
+
if object.nil?
|
27
|
+
msg = "#{File.basename(config_file)} does not specify required element #{elements.map { |x| "['#{x}']"}.join('')}"
|
28
|
+
raise MissingConfiguration, msg
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
class Directory
|
3
|
+
attr_reader :authorities, :my_private_key, :my_authority_name
|
4
|
+
|
5
|
+
def initialize(keystore_directory)
|
6
|
+
certs = Dir[File.join(keystore_directory, '*.pub')]
|
7
|
+
keys = Dir[File.join(keystore_directory, '*.key')]
|
8
|
+
|
9
|
+
@authorities = {}
|
10
|
+
certs.each do |cert_file|
|
11
|
+
basename = File.basename(cert_file)
|
12
|
+
authority = basename[0...(basename.rindex('.'))] #chop trailing .ext
|
13
|
+
@authorities[authority] = OpenSSL::PKey::RSA.new(File.read(cert_file))
|
14
|
+
raise TypeError, "Expected #{basename} to contain an RSA public key" unless @authorities[authority].public?
|
15
|
+
end
|
16
|
+
|
17
|
+
raise ArgumentError, "Excepted 0 or 1 key files, found #{keys.size}" if ![0, 1].include?(keys.size)
|
18
|
+
if (key_file = keys[0])
|
19
|
+
basename = File.basename(key_file)
|
20
|
+
@my_private_key = OpenSSL::PKey::RSA.new(File.read(key_file))
|
21
|
+
raise TypeError, "Expected #{basename} to contain an RSA private key" unless @my_private_key.private?
|
22
|
+
@my_authority_name = basename[0...(basename.rindex('.'))] #chop trailing .ext
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def invalidated_session?(uuid)
|
27
|
+
false
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# Standard library dependencies
|
2
|
+
require 'set'
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
# Gem dependencies
|
6
|
+
require 'uuidtools'
|
7
|
+
|
8
|
+
module HasGlobalSession
|
9
|
+
class GlobalSession
|
10
|
+
attr_reader :id, :authority, :created_at, :expires_at
|
11
|
+
|
12
|
+
def initialize(directory, cookie=nil)
|
13
|
+
@schema_signed = Set.new((Configuration['attributes']['signed'] rescue []))
|
14
|
+
@schema_insecure = Set.new((Configuration['attributes']['insecure'] rescue []))
|
15
|
+
@directory = directory
|
16
|
+
|
17
|
+
if cookie
|
18
|
+
#User presented us with a cookie; let's decrypt and verify it
|
19
|
+
zbin = Base64.decode64(cookie)
|
20
|
+
json = Zlib::Inflate.inflate(zbin)
|
21
|
+
hash = ActiveSupport::JSON.decode(json)
|
22
|
+
@id = hash['id']
|
23
|
+
@created_at = Time.at(hash['tc'].to_i)
|
24
|
+
@expires_at = Time.at(hash['te'].to_i)
|
25
|
+
@signed = hash['ds']
|
26
|
+
@insecure = hash['dx']
|
27
|
+
@signature = hash['s']
|
28
|
+
@authority = hash['a']
|
29
|
+
|
30
|
+
hash.delete('s')
|
31
|
+
expected = digest(hash)
|
32
|
+
signer = @directory.authorities[@authority]
|
33
|
+
raise SecurityError, "Unknown signing authority #{@authority}" unless signer
|
34
|
+
got = signer.public_decrypt(Base64.decode64(@signature))
|
35
|
+
unless (got == expected)
|
36
|
+
raise SecurityError, "Signature mismatch on global session cookie; tampering suspected"
|
37
|
+
end
|
38
|
+
|
39
|
+
if expired? || @directory.invalidated_session?(@id)
|
40
|
+
raise ExpiredSession, "Global session cookie has expired"
|
41
|
+
end
|
42
|
+
|
43
|
+
else
|
44
|
+
@signed = {}
|
45
|
+
@insecure = {}
|
46
|
+
@id = UUIDTools::UUID.timestamp_create.to_s
|
47
|
+
@created_at = Time.now.utc
|
48
|
+
@expires_at = 2.hours.from_now.utc #TODO configurable
|
49
|
+
@authority = @directory.my_authority_name
|
50
|
+
@dirty_secure = true
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def supports_key?(key)
|
55
|
+
@schema_signed.include?(key) || @schema_insecure.include?(key)
|
56
|
+
end
|
57
|
+
|
58
|
+
def expired?
|
59
|
+
(@expires_at <= Time.now)
|
60
|
+
end
|
61
|
+
|
62
|
+
def expire!
|
63
|
+
@expires_at = Time.at(0)
|
64
|
+
@dirty_secure = true
|
65
|
+
end
|
66
|
+
|
67
|
+
def to_s
|
68
|
+
hash = {'id'=>@id,
|
69
|
+
'tc'=>@created_at.to_i, 'te'=>@expires_at.to_i,
|
70
|
+
'ds'=>@signed, 'dx'=>@insecure}
|
71
|
+
|
72
|
+
if @signature && !@dirty_secure
|
73
|
+
#use cached signature unless we've changed secure state
|
74
|
+
authority = @authority
|
75
|
+
signature = @signature
|
76
|
+
else
|
77
|
+
authority = @directory.my_authority_name
|
78
|
+
hash['a'] = authority
|
79
|
+
digest = digest(hash)
|
80
|
+
signature = Base64.encode64(@directory.my_private_key.private_encrypt(digest))
|
81
|
+
end
|
82
|
+
|
83
|
+
hash['s'] = signature
|
84
|
+
hash['a'] = authority
|
85
|
+
json = ActiveSupport::JSON.encode(hash)
|
86
|
+
zbin = Zlib::Deflate.deflate(json, Zlib::BEST_COMPRESSION)
|
87
|
+
return Base64.encode64(zbin)
|
88
|
+
end
|
89
|
+
|
90
|
+
def [](key)
|
91
|
+
@signed[key] || @insecure[key]
|
92
|
+
end
|
93
|
+
|
94
|
+
def []=(key, value)
|
95
|
+
if @schema_signed.include?(key)
|
96
|
+
unless @directory.my_private_key && @directory.my_authority_name
|
97
|
+
raise StandardError, 'Cannot change secure session attributes; we are not an authority'
|
98
|
+
end
|
99
|
+
|
100
|
+
@signed[key] = value
|
101
|
+
@dirty_secure = true
|
102
|
+
elsif @schema_insecure.include?(key)
|
103
|
+
@insecure[key] = value
|
104
|
+
else
|
105
|
+
raise ArgumentError, "Attribute '#{key}' is not specified in global session configuration"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def has_key?(key)
|
110
|
+
@signed.has_key(key) || @insecure.has_key?(key)
|
111
|
+
end
|
112
|
+
|
113
|
+
def keys
|
114
|
+
@signed.keys + @insecure.keys
|
115
|
+
end
|
116
|
+
|
117
|
+
def values
|
118
|
+
@signed.values + @insecure.values
|
119
|
+
end
|
120
|
+
|
121
|
+
def each_pair(&block)
|
122
|
+
@signed.each_pair(&block)
|
123
|
+
@insecure.each_pair(&block)
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def digest(input)
|
129
|
+
canonical = ActiveSupport::JSON.encode(canonicalize(input))
|
130
|
+
return Digest::SHA1.new().update(canonical).hexdigest
|
131
|
+
end
|
132
|
+
|
133
|
+
def canonicalize(input)
|
134
|
+
case input
|
135
|
+
when Hash
|
136
|
+
output = ActiveSupport::OrderedHash.new
|
137
|
+
ordered_keys = input.keys.sort
|
138
|
+
ordered_keys.each do |key|
|
139
|
+
output[canonicalize(key)] = canonicalize(input[key])
|
140
|
+
end
|
141
|
+
when Array
|
142
|
+
output = input.collect { |x| canonicalize(x) }
|
143
|
+
when Numeric, String, ActiveSupport::OrderedHash
|
144
|
+
output = input
|
145
|
+
else
|
146
|
+
raise TypeError, "Objects of type #{input.class.name} cannot be serialized in the global session"
|
147
|
+
end
|
148
|
+
|
149
|
+
return output
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
class IntegratedSession
|
3
|
+
def initialize(local_session, global_session)
|
4
|
+
@local_session = local_session
|
5
|
+
@global_session = global_session
|
6
|
+
end
|
7
|
+
|
8
|
+
def [](key)
|
9
|
+
if @global_session.supports_key?(key)
|
10
|
+
@global_session[key]
|
11
|
+
else
|
12
|
+
@local_session[key]
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(key, value)
|
17
|
+
if @global_session.supports_key?(key)
|
18
|
+
@global_session[key] = value
|
19
|
+
else
|
20
|
+
@local_session[key] = value
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def has_key?(key)
|
25
|
+
@global_session.has_key(key) || @local_session.has_key?(key)
|
26
|
+
end
|
27
|
+
|
28
|
+
def keys
|
29
|
+
@global_session.keys + @local_session.keys
|
30
|
+
end
|
31
|
+
|
32
|
+
def values
|
33
|
+
@global_session.values + @local_session.values
|
34
|
+
end
|
35
|
+
|
36
|
+
def each_pair(&block)
|
37
|
+
@global_session.each_pair(&block)
|
38
|
+
@local_session.each_pair(&block)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
class MissingConfiguration < Exception; end
|
3
|
+
class SessionExpired < Exception; end
|
4
|
+
end
|
5
|
+
|
6
|
+
basedir = File.dirname(__FILE__)
|
7
|
+
require File.join(basedir, 'has_global_session', 'configuration')
|
8
|
+
require File.join(basedir, 'has_global_session', 'directory')
|
9
|
+
require File.join(basedir, 'has_global_session', 'global_session')
|
10
|
+
require File.join(basedir, 'has_global_session', 'integrated_session')
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module HasGlobalSession
|
2
|
+
module ActionControllerInstanceMethods
|
3
|
+
def global_session
|
4
|
+
return @global_session if @global_session
|
5
|
+
|
6
|
+
begin
|
7
|
+
cookie = cookies[Configuration['cookie']['name']]
|
8
|
+
directory = Directory.new(File.join(RAILS_ROOT, 'config', 'authorities'))
|
9
|
+
|
10
|
+
begin
|
11
|
+
#unserialize the global session from the cookie, or
|
12
|
+
#initialize a new global session if cookie == nil
|
13
|
+
@global_session = GlobalSession.new(directory, cookie)
|
14
|
+
rescue SessionExpired
|
15
|
+
#if the cookie is present but expired, silently
|
16
|
+
#initialize a new global session
|
17
|
+
@global_session = GlobalSession.new(directory)
|
18
|
+
end
|
19
|
+
rescue Exception => e
|
20
|
+
cookies.delete Configuration['cookie']['name']
|
21
|
+
raise e
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
if Configuration['integrated']
|
26
|
+
def session
|
27
|
+
@integrated_session ||= IntegratedSession.new(super, global_session)
|
28
|
+
return @integrated_session
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def global_session_update_cookie
|
33
|
+
if @global_session
|
34
|
+
if @global_session.expired?
|
35
|
+
options = {:value => nil,
|
36
|
+
:domain => Configuration['cookie']['domain'],
|
37
|
+
:expires => Time.at(0)}
|
38
|
+
else
|
39
|
+
options = {:value => @global_session.to_s,
|
40
|
+
:domain => Configuration['cookie']['domain'],
|
41
|
+
:expires => @global_session.expires_at}
|
42
|
+
end
|
43
|
+
|
44
|
+
cookies[Configuration['cookie']['name']] = options
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
basedir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
2
|
+
require File.join(basedir, 'lib', 'has_global_session')
|
3
|
+
|
4
|
+
# Tie the Configuration module to Rails' filesystem structure
|
5
|
+
# and operating environment.
|
6
|
+
HasGlobalSession::Configuration.config_file =
|
7
|
+
File.join(RAILS_ROOT, 'config', 'global_session.yml')
|
8
|
+
HasGlobalSession::Configuration.environment = RAILS_ENV
|
9
|
+
|
10
|
+
require File.join(basedir, 'rails', 'action_controller_instance_methods')
|
11
|
+
|
12
|
+
# Enable ActionController integration.
|
13
|
+
class ActionController::Base
|
14
|
+
def self.has_global_session
|
15
|
+
include HasGlobalSession::ActionControllerInstanceMethods
|
16
|
+
after_filter :global_session_update_cookie
|
17
|
+
end
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: has_global_session
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.8.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tony Spataro
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2010-06-01 00:00:00 -07:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: uuidtools
|
17
|
+
type: :runtime
|
18
|
+
version_requirement:
|
19
|
+
version_requirements: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 2.1.1
|
24
|
+
version:
|
25
|
+
description: This Rails plugin allows several Rails web apps that share the same back-end user database to share session state in a cryptographically secure way, facilitating single sign-on in a distributed web app. It only provides session sharing and does not concern itself with authentication or replication of the user database.
|
26
|
+
email: code@tracker.xeger.net
|
27
|
+
executables: []
|
28
|
+
|
29
|
+
extensions: []
|
30
|
+
|
31
|
+
extra_rdoc_files: []
|
32
|
+
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README
|
36
|
+
- has_global_session.gemspec
|
37
|
+
- init.rb
|
38
|
+
- lib/generators/global_session_config/USAGE
|
39
|
+
- lib/generators/global_session_config/global_session_config_generator.rb
|
40
|
+
- lib/generators/global_session_config/templates/global_session.yml.erb
|
41
|
+
- lib/has_global_session.rb
|
42
|
+
- lib/has_global_session/configuration.rb
|
43
|
+
- lib/has_global_session/directory.rb
|
44
|
+
- lib/has_global_session/global_session.rb
|
45
|
+
- lib/has_global_session/integrated_session.rb
|
46
|
+
- rails/action_controller_instance_methods.rb
|
47
|
+
- rails/init.rb
|
48
|
+
has_rdoc: true
|
49
|
+
homepage: http://github.com/xeger/has_global_session
|
50
|
+
licenses: []
|
51
|
+
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.8.7
|
62
|
+
version:
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
version:
|
69
|
+
requirements: []
|
70
|
+
|
71
|
+
rubyforge_project:
|
72
|
+
rubygems_version: 1.3.5
|
73
|
+
signing_key:
|
74
|
+
specification_version: 3
|
75
|
+
summary: Secure single-domain session sharing plugin for Rails.
|
76
|
+
test_files: []
|
77
|
+
|