class Rack::Session::Cookie

Rack::Session::Cookie provides simple cookie based session management. By default, the session is a Ruby Hash that is serialized and encoded as a cookie set to :key (default: rack.session).

This middleware accepts a :secrets option which enables encryption of session cookies. This option should be one or more random “secret keys” that are each at least 64 bytes in length. Multiple secret keys can be supplied in an Array, which is useful when rotating secrets.

Several options are also accepted that are passed to Rack::Session::Encryptor. These options include:

Refer to Rack::Session::Encryptor for more details on these options.

Prior to version TODO, the session hash was stored as base64 encoded marshalled data. When a :secret option was supplied, the integrity of the encoded data was protected with HMAC-SHA1. This functionality is still supported using a set of a legacy options.

Lastly, a :coder option is also accepted. When used, both encryption and the legacy HMAC will be skipped. This option could create security issues in your application!

Example:

use Rack::Session::Cookie, {
  key: 'rack.session',
  domain: 'foo.com',
  path: '/',
  expire_after: 2592000,
  secrets: 'a randomly generated, raw binary string 64 bytes in size',
}

Example using legacy HMAC options:

Rack::Session:Cookie.new(application, {
  # The secret used for legacy HMAC cookies, this enables the functionality
  legacy_hmac_secret: 'legacy secret',
  # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
  legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
  # legacy_hmac will default to OpenSSL::Digest::SHA1
  legacy_hmac: OpenSSL::Digest::SHA256
})

Example of a cookie with no encoding:

Rack::Session::Cookie.new(application, {
  :coder => Rack::Session::Cookie::Identity.new
})

Example of a cookie with custom encoding:

Rack::Session::Cookie.new(application, {
  :coder => Class.new {
    def encode(str); str.reverse; end
    def decode(str); str.reverse; end
  }.new
})

Attributes

coder[R]
encryptors[R]

Public Class Methods

new(app, options = {}) click to toggle source
Calls superclass method Rack::Session::Abstract::Persisted::new
# File lib/rack/session/cookie.rb, line 159
      def initialize(app, options = {})
        # support both :secrets and :secret for backwards compatibility
        secrets = [*(options[:secrets] || options[:secret])]

        encryptor_opts = {
          purpose: options[:key], serialize_json: options[:serialize_json]
        }

        # For each secret, create an Encryptor. We have iterate this Array at
        # decryption time to achieve key rotation.
        @encryptors = secrets.map do |secret|
          Rack::Session::Encryptor.new secret, encryptor_opts
        end

        # If a legacy HMAC secret is present, initialize those features.
        # Fallback to :secret for backwards compatibility.
        if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret)
          @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')

          @legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret]
          @legacy_hmac_coder  = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
        else
          @legacy_hmac = false
        end

        warn <<-MSG unless secure?(options)
        SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
        This poses a security threat. It is strongly recommended that you
        provide a secret to prevent exploits that may be possible from crafted
        cookies. This will not be supported in future versions of Rack, and
        future versions will even invalidate your existing user cookies.

        Called from: #{caller[0]}.
        MSG

        # Potential danger ahead! Marshal without verification and/or
        # encryption could present a major security issue.
        @coder = options[:coder] ||= Base64::Marshal.new

        super(app, options.merge!(cookie_only: true))
      end

Private Instance Methods

delete_session(req, session_id, options) click to toggle source
# File lib/rack/session/cookie.rb, line 277
def delete_session(req, session_id, options)
  # Nothing to do here, data is in the client
  generate_sid unless options[:drop]
end
encode_session_data(session) click to toggle source
# File lib/rack/session/cookie.rb, line 292
def encode_session_data(session)
  if encryptors.empty?
    coder.encode(session)
  else
    encryptors.first.encrypt(session)
  end
end
extract_session_id(request) click to toggle source
# File lib/rack/session/cookie.rb, line 209
def extract_session_id(request)
  unpacked_cookie_data(request)&.[]("session_id")
end
find_session(req, sid) click to toggle source
# File lib/rack/session/cookie.rb, line 203
def find_session(req, sid)
  data = unpacked_cookie_data(req)
  data = persistent_session_id!(data)
  [data["session_id"], data]
end
legacy_digest_match?(data, digest) click to toggle source
# File lib/rack/session/cookie.rb, line 282
def legacy_digest_match?(data, digest)
  return false unless data && digest

  Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
end
legacy_generate_hmac(data) click to toggle source
# File lib/rack/session/cookie.rb, line 288
def legacy_generate_hmac(data)
  OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
end
persistent_session_id!(data, sid = nil) click to toggle source
# File lib/rack/session/cookie.rb, line 250
def persistent_session_id!(data, sid = nil)
  data ||= {}
  data["session_id"] ||= sid || generate_sid
  data
end
secure?(options) click to toggle source

Were consider “secure” if:

  • Encrypted cookies are enabled and one or more encryptor is initialized

  • The legacy HMAC option is enabled

  • Customer :coder is used, with :let_coder_handle_secure_encoding set to true

# File lib/rack/session/cookie.rb, line 306
def secure?(options)
  !@encryptors.empty? ||
    @legacy_hmac ||
    (options[:coder] && options[:let_coder_handle_secure_encoding])
end
write_session(req, session_id, session, options) click to toggle source
# File lib/rack/session/cookie.rb, line 265
def write_session(req, session_id, session, options)
  session = session.merge("session_id" => session_id)
  session_data = encode_session_data(session)

  if session_data.size > (4096 - @key.size)
    req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
    nil
  else
    SessionId.new(session_id, session_data)
  end
end