Module: Roda::RodaPlugins::Sessions
- Defined in:
- lib/roda/plugins/sessions.rb
Overview
The sessions plugin adds support for sessions using cookies. It is the recommended way to support sessions in Roda applications.
The session cookies are encrypted with AES-256-CTR using a separate encryption key per cookie, and then signed with HMAC-SHA-256. By default, session data is padded to reduce information leaked based on the session size.
Sessions are serialized via JSON, so session information should only store data that allows roundtrips via JSON (String, Integer, Float, Array, Hash, true, false, and nil). In particular, note that Symbol does not round trip via JSON, so symbols should not be used in sessions when this plugin is used. This plugin sets the :sessions_convert_symbols
application option to true
if it hasn’t been set yet, for better integration with plugins that can use either symbol or string session or flash keys. Unlike Rack::Session::Cookie, the session is stored as a plain ruby hash, and does not convert all keys to strings.
All sessions are timestamped and session expiration is enabled by default, with sessions being valid for 30 days maximum and 7 days since last use by default. Session creation time is reset whenever the session is empty when serialized and also whenever clear_session
is called while processing the request.
Session secrets can be rotated. See options below.
The sessions plugin can transparently upgrade sessions from versions of Rack::Session::Cookie shipped with Rack before Rack 3, if the default Rack::Session::Cookie coder and HMAC are used, see options below. It is recommended to only enable transparent upgrades for a brief transition period, and remove support for them once old sessions have converted or timed out.
If the final cookie is too large (>=4096 bytes), a Roda::RodaPlugins::Sessions::CookieTooLarge exception will be raised.
Required Options
The session cookies this plugin uses are both encrypted and signed, so two separate secrets are used internally. However, for ease of use, these secrets are combined into a single :secret
option. The :secret
option must be a string of at least 64 bytes and should be randomly generated. The first 32 bytes are used as the secret for the cipher, any remaining bytes are used for the secret for the HMAC.
Other Options
- :cookie_options
-
Any cookie options to set on the session cookie. By default, uses
httponly: true, path: '/', same_site: :lax
so that the cookie is not accessible to javascript, allowed for all paths, and will not be used for cross-site non-GET requests that. If the:secure
option is not present in the hash, thensecure: true
is also set if the request is made over HTTPS. If this option is given, it will be merged into the default cookie options. - :gzip_over
-
For session data over this many bytes, compress it with the deflate algorithm (default: nil, so never compress). Note that compression should not be enabled if you are storing data in the session derived from user input and also storing sensitive data in the session.
- :key
-
The cookie name to use (default:
'roda.session'
) - :max_seconds
-
The maximum number of seconds to allow for total session lifetime, starting with when the session was originally created. Default is
86400*30
(30 days). Can be set tonil
to disable session lifetime checks. - :max_idle_seconds
-
The maximum number of seconds to allow since the session was last updated. Default is
86400*7
(7 days). Can be set to nil to disable session idleness checks. - :old_secret
-
The previous secret to use, allowing for secret rotation. Must be a string of at least 64 bytes if given.
- :pad_size
-
Pad session data (after possible compression, before encryption), to a multiple of this many bytes (default: 32). This can be between 2-4096 bytes, or
nil
to disable padding. - :per_cookie_cipher_secret
-
Uses a separate cipher key for every cookie, with the key used generated using HMAC-SHA-256 of 32 bytes of random data with the default cipher secret. This offers additional protection in case the random initialization vector used when encrypting the session data has been reused. Odds of that are 1 in 2**64 if initialization vector is truly random, but weaknesses in the random number generator could make the odds much higher. Default is
true
. - :parser
-
The parser for the serialized session data (default:
JSON.method(:parse)
). - :serializer
-
The serializer for the session data (default
:to_json.to_proc
). - :skip_within
-
If the last update time for the session cookie is less than this number of seconds from the current time, and the session has not been modified, do not set a new session cookie (default: 3600).
- :upgrade_from_rack_session_cookie_key
-
The cookie name to use for transparently upgrading from Rack::Session:Cookie (defaults to
'rack.session'
). - :upgrade_from_rack_session_cookie_secret
-
The secret for the HMAC-SHA1 signature when allowing transparent upgrades from Rack::Session::Cookie. Using this option is only recommended during a short transition period, and is not enabled by default as it lowers security.
- :upgrade_from_rack_session_cookie_options
-
Options to pass when deleting the cookie used by Rack::Session::Cookie after converting it to use the session cookies used by this plugin.
Not a Rack Middleware
Unlike some other approaches to sessions, the sessions plugin does not use a rack middleware, so session information is not available to other rack middleware, only to the application itself, with the session not being loaded from the cookie until the session
method is called.
If you need rack middleware to access the session information, then require 'roda/session_middleware'
and use RodaSessionMiddleware
. RodaSessionMiddleware
passes the options given to this plugin.
Session Cookie Cryptography/Format
Session cookies created by this plugin by default use the following format:
urlsafe_base64("\1" + random_data + IV + encrypted session data + HMAC)
If :per_cookie_cipher_secret
option is set to false
, an older format is used:
urlsafe_base64("\0" + IV + encrypted session data + HMAC)
where:
- version
-
1 byte, currently must be 1 or 0, other values reserved for future expansion.
- random_data
-
32 bytes, used for generating the per-cookie secret
- IV
-
16 bytes, initialization vector for AES-256-CTR cipher.
- encrypted session data
-
>=12 bytes of data encrypted with AES-256-CTR cipher, see below.
- HMAC
-
32 bytes, HMAC-SHA-256 of all preceding data plus cookie key (so that a cookie value for a different key cannot be used even if the secret is the same).
The encrypted session data uses the following format:
bitmap + creation time + update time + padding + serialized data
where:
- bitmap
-
2 bytes in little endian format, lower 12 bits storing number of padding bytes, 13th bit storing whether serialized data is compressed with deflate. Bits 14-16 reserved for future expansion.
- creation time
-
4 byte integer in unsigned little endian format, storing unix timestamp since session initially created.
- update time
-
4 byte integer in unsigned little endian format, storing unix timestamp since session last updated.
- padding
-
>=0 padding bytes specified in bitmap, filled with random data, can be ignored.
- serialized data
-
>=2 bytes of serialized data in JSON format. If the bitmap indicates deflate compression, this contains the deflate compressed data.
Defined Under Namespace
Modules: InstanceMethods, RequestMethods Classes: CookieTooLarge
Constant Summary collapse
- DEFAULT_COOKIE_OPTIONS =
{:httponly=>true, :path=>'/'.freeze, :same_site=>:lax}.freeze
- DEFAULT_OPTIONS =
{:key => 'roda.session'.freeze, :max_seconds=>86400*30, :max_idle_seconds=>86400*7, :pad_size=>32, :gzip_over=>nil, :skip_within=>3600}.freeze
- DEFLATE_BIT =
0x1000
- PADDING_MASK =
0x0fff
- SESSION_CREATED_AT =
'roda.session.created_at'.freeze
- SESSION_UPDATED_AT =
'roda.session.updated_at'.freeze
- SESSION_SERIALIZED =
'roda.session.serialized'.freeze
- SESSION_VERSION_NUM =
'roda.session.version'.freeze
- SESSION_DELETE_RACK_COOKIE =
'roda.session.delete_rack_session_cookie'.freeze
Class Method Summary collapse
-
.configure(app, opts = OPTS) ⇒ Object
Configure the plugin, see Sessions for details on options.
- .load_dependencies(app, opts = OPTS) ⇒ Object
-
.split_secret(name, secret) ⇒ Object
Split given secret into a cipher secret and an hmac secret.
Class Method Details
.configure(app, opts = OPTS) ⇒ Object
Configure the plugin, see Sessions for details on options.
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 |
# File 'lib/roda/plugins/sessions.rb', line 179 def self.configure(app, opts=OPTS) opts = (app.opts[:sessions] || DEFAULT_OPTIONS).merge(opts) co = opts[:cookie_options] = DEFAULT_COOKIE_OPTIONS.merge(opts[:cookie_options] || OPTS).freeze opts[:remove_cookie_options] = co.merge(:max_age=>'0', :expires=>Time.at(0)) opts[:parser] ||= app.opts[:json_parser] || JSON.method(:parse) opts[:serializer] ||= app.opts[:json_serializer] || :to_json.to_proc opts[:per_cookie_cipher_secret] = true unless opts.has_key?(:per_cookie_cipher_secret) opts[:session_version_num] = opts[:per_cookie_cipher_secret] ? 1 : 0 if opts[:upgrade_from_rack_session_cookie_secret] opts[:upgrade_from_rack_session_cookie_key] ||= 'rack.session' rsco = opts[:upgrade_from_rack_session_cookie_options] = Hash[opts[:upgrade_from_rack_session_cookie_options] || OPTS] rsco[:path] ||= co[:path] rsco[:domain] ||= co[:domain] end opts[:cipher_secret], opts[:hmac_secret] = split_secret(:secret, opts[:secret]) opts[:old_cipher_secret], opts[:old_hmac_secret] = (split_secret(:old_secret, opts[:old_secret]) if opts[:old_secret]) case opts[:pad_size] when nil # no changes when Integer raise RodaError, "invalid :pad_size: #{opts[:pad_size]}, must be >=2, < 4096" unless opts[:pad_size] >= 2 && opts[:pad_size] < 4096 else raise RodaError, "invalid :pad_size option: #{opts[:pad_size].inspect}, must be Integer or nil" end app.opts[:sessions] = opts.freeze app.opts[:sessions_convert_symbols] = true unless app.opts.has_key?(:sessions_convert_symbols) end |
.load_dependencies(app, opts = OPTS) ⇒ Object
174 175 176 |
# File 'lib/roda/plugins/sessions.rb', line 174 def self.load_dependencies(app, opts=OPTS) app.plugin :_base64 end |
.split_secret(name, secret) ⇒ Object
Split given secret into a cipher secret and an hmac secret.
166 167 168 169 170 171 172 |
# File 'lib/roda/plugins/sessions.rb', line 166 def self.split_secret(name, secret) raise RodaError, "sessions plugin :#{name} option must be a String" unless secret.is_a?(String) raise RodaError, "invalid sessions plugin :#{name} option length: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64 hmac_secret = secret = secret.dup.force_encoding('BINARY') cipher_secret = secret.slice!(0, 32) [cipher_secret.freeze, hmac_secret.freeze] end |