has_global_session 0.8.0
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/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
|
+
|