ktheory-json_cookie_store 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +3 -0
- data/lib/json_cookie_store.rb +175 -0
- metadata +63 -0
data/README.rdoc
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'cgi/session'
|
3
|
+
require 'openssl' # to generate the HMAC message digest
|
4
|
+
# Requiring JSON here causese complications between Rails' json methods
|
5
|
+
# and the JSON gem. Not requiring 'json' works. IDK why. --Aaron
|
6
|
+
#require 'json'
|
7
|
+
|
8
|
+
# This cookie-based session store is the Rails default. Sessions typically
|
9
|
+
# contain at most a user_id and flash message; both fit within the 4K cookie
|
10
|
+
# size limit. Cookie-based sessions are dramatically faster than the
|
11
|
+
# alternatives.
|
12
|
+
#
|
13
|
+
# If you have more than 4K of session data or don't want your data to be
|
14
|
+
# visible to the user, pick another session store.
|
15
|
+
#
|
16
|
+
# CookieOverflow is raised if you attempt to store more than 4K of data.
|
17
|
+
# TamperedWithCookie is raised if the data integrity check fails.
|
18
|
+
#
|
19
|
+
# A message digest is included with the cookie to ensure data integrity:
|
20
|
+
# a user cannot alter his +user_id+ without knowing the secret key included in
|
21
|
+
# the hash. New apps are generated with a pregenerated secret in
|
22
|
+
# config/environment.rb. Set your own for old apps you're upgrading.
|
23
|
+
#
|
24
|
+
# Session options:
|
25
|
+
#
|
26
|
+
# * <tt>:secret</tt>: An application-wide key string or block returning a string
|
27
|
+
# called per generated digest. The block is called with the CGI::Session
|
28
|
+
# instance as an argument. It's important that the secret is not vulnerable to
|
29
|
+
# a dictionary attack. Therefore, you should choose a secret consisting of
|
30
|
+
# random numbers and letters and more than 30 characters. Examples:
|
31
|
+
#
|
32
|
+
# :secret => '449fe2e7daee471bffae2fd8dc02313d'
|
33
|
+
# :secret => Proc.new { User.current_user.secret_key }
|
34
|
+
#
|
35
|
+
# * <tt>:digest</tt>: The message digest algorithm used to verify session
|
36
|
+
# integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
|
37
|
+
# such as 'MD5', 'RIPEMD160', 'SHA256', etc.
|
38
|
+
#
|
39
|
+
# To generate a secret key for an existing application, run
|
40
|
+
# "rake secret" and set the key in config/environment.rb.
|
41
|
+
#
|
42
|
+
# Note that changing digest or secret invalidates all existing sessions!
|
43
|
+
class CGI::Session::JsonCookieStore
|
44
|
+
# Cookies can typically store 4096 bytes.
|
45
|
+
MAX = 4096
|
46
|
+
SECRET_MIN_LENGTH = 30 # characters
|
47
|
+
|
48
|
+
# Raised when storing more than 4K of session data.
|
49
|
+
class CookieOverflow < StandardError; end
|
50
|
+
|
51
|
+
# Raised when the cookie fails its integrity check.
|
52
|
+
class TamperedWithCookie < StandardError; end
|
53
|
+
|
54
|
+
# Called from CGI::Session only.
|
55
|
+
def initialize(session, options = {})
|
56
|
+
# The session_key option is required.
|
57
|
+
if options['session_key'].blank?
|
58
|
+
raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
|
59
|
+
end
|
60
|
+
|
61
|
+
# The secret option is required.
|
62
|
+
ensure_secret_secure(options['secret'])
|
63
|
+
|
64
|
+
# Keep the session and its secret on hand so we can read and write cookies.
|
65
|
+
@session, @secret = session, options['secret']
|
66
|
+
|
67
|
+
# Message digest defaults to SHA1.
|
68
|
+
@digest = options['digest'] || 'SHA1'
|
69
|
+
|
70
|
+
# Default cookie options derived from session settings.
|
71
|
+
@cookie_options = {
|
72
|
+
'name' => options['session_key'],
|
73
|
+
'path' => options['session_path'],
|
74
|
+
'domain' => options['session_domain'],
|
75
|
+
'expires' => options['session_expires'],
|
76
|
+
'secure' => options['session_secure'],
|
77
|
+
'http_only' => options['session_http_only']
|
78
|
+
}
|
79
|
+
|
80
|
+
# Set no_hidden and no_cookies since the session id is unused and we
|
81
|
+
# set our own data cookie.
|
82
|
+
options['no_hidden'] = true
|
83
|
+
options['no_cookies'] = true
|
84
|
+
end
|
85
|
+
|
86
|
+
# To prevent users from using something insecure like "Password" we make sure that the
|
87
|
+
# secret they've provided is at least 30 characters in length.
|
88
|
+
def ensure_secret_secure(secret)
|
89
|
+
# There's no way we can do this check if they've provided a proc for the
|
90
|
+
# secret.
|
91
|
+
return true if secret.is_a?(Proc)
|
92
|
+
|
93
|
+
if secret.blank?
|
94
|
+
raise ArgumentError, %Q{A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
|
95
|
+
end
|
96
|
+
|
97
|
+
if secret.length < SECRET_MIN_LENGTH
|
98
|
+
raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}". The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
# Restore session data from the cookie.
|
103
|
+
def restore
|
104
|
+
@original = read_cookie
|
105
|
+
@data = unmarshal(@original) || {}
|
106
|
+
end
|
107
|
+
|
108
|
+
# Wait until close to write the session data cookie.
|
109
|
+
def update; end
|
110
|
+
|
111
|
+
# Write the session data cookie if it was loaded and has changed.
|
112
|
+
def close
|
113
|
+
if defined?(@data) && !@data.blank?
|
114
|
+
updated = marshal(@data)
|
115
|
+
raise CookieOverflow if updated.size > MAX
|
116
|
+
write_cookie('value' => updated) unless updated == @original
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Delete the session data by setting an expired cookie with no data.
|
121
|
+
def delete
|
122
|
+
@data = nil
|
123
|
+
clear_old_cookie_value
|
124
|
+
write_cookie('value' => nil, 'expires' => 1.year.ago)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Generate the HMAC keyed message digest. Uses SHA1 by default.
|
128
|
+
def generate_digest(data)
|
129
|
+
key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
|
130
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
# Marshal a session hash into safe cookie data. Include an integrity hash.
|
135
|
+
def marshal(session)
|
136
|
+
data = ActiveSupport::Base64.encode64s(::JSON.dump(session))
|
137
|
+
"#{data}--#{generate_digest(data)}"
|
138
|
+
end
|
139
|
+
|
140
|
+
# Unmarshal cookie data to a hash and verify its integrity.
|
141
|
+
def unmarshal(cookie)
|
142
|
+
if cookie
|
143
|
+
data, digest = cookie.split('--')
|
144
|
+
|
145
|
+
# Do two checks to transparently support old double-escaped data.
|
146
|
+
unless digest == generate_digest(data) || digest == generate_digest(data = CGI.unescape(data))
|
147
|
+
delete
|
148
|
+
raise TamperedWithCookie
|
149
|
+
end
|
150
|
+
|
151
|
+
res = ::JSON.load(ActiveSupport::Base64.decode64(data))
|
152
|
+
# Yay for FlashHash hacks!
|
153
|
+
# Convert res['flash'] to a FlashHash
|
154
|
+
# Note this breaks Flash functionality
|
155
|
+
res['flash'] = ActionController::Flash::FlashHash.new
|
156
|
+
res
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Read the session data cookie.
|
161
|
+
def read_cookie
|
162
|
+
@session.cgi.cookies[@cookie_options['name']].first
|
163
|
+
end
|
164
|
+
|
165
|
+
# CGI likes to make you hack.
|
166
|
+
def write_cookie(options)
|
167
|
+
cookie = CGI::Cookie.new(@cookie_options.merge(options))
|
168
|
+
@session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
|
169
|
+
end
|
170
|
+
|
171
|
+
# Clear cookie value so subsequent new_session doesn't reload old data.
|
172
|
+
def clear_old_cookie_value
|
173
|
+
@session.cgi.cookies[@cookie_options['name']].clear
|
174
|
+
end
|
175
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ktheory-json_cookie_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Aaron Suggs
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-12 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rails
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - "="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: 2.2.2
|
23
|
+
version:
|
24
|
+
description: Like Rails' CookieStore for sessions, but uses JSON format rather than Marshaled ruby objects
|
25
|
+
email: aaronktheory.com
|
26
|
+
executables: []
|
27
|
+
|
28
|
+
extensions: []
|
29
|
+
|
30
|
+
extra_rdoc_files:
|
31
|
+
- README.rdoc
|
32
|
+
files:
|
33
|
+
- lib/json_cookie_store.rb
|
34
|
+
- README.rdoc
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/ktheory/json_cookie_store
|
37
|
+
post_install_message:
|
38
|
+
rdoc_options:
|
39
|
+
- --main
|
40
|
+
- README.rdoc
|
41
|
+
require_paths:
|
42
|
+
- lib
|
43
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: "0"
|
48
|
+
version:
|
49
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: "0"
|
54
|
+
version:
|
55
|
+
requirements: []
|
56
|
+
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 1.2.0
|
59
|
+
signing_key:
|
60
|
+
specification_version: 2
|
61
|
+
summary: JSON cookie store for Rails' sessions
|
62
|
+
test_files: []
|
63
|
+
|