Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

add Authlogic::Session::Cookies#sign_cookies config option #342

Merged
merged 1 commit into from

3 participants

@tiegz
Collaborator

WHAT

Add a config option to cookies to use Rails-style signed cookies for the remember_me cookie (disabled by default).

WHY

Just as an extra precaution to take, if desired.

GOTCHA

When this switch is toggled, already-existing cookies in the wild will be invalidated.

Thoughts?

@tiegz tiegz referenced this pull request
Closed

Option to use signed cookies #331

@drewish

Seems like this might address #309

@tiegz
Collaborator

@drewish I'm in favor of locking down the remember-me cookie from replay attacks, but currently you could do that by setting the secure option to true and calling reset_persistence_token in your own app more often. Unfortunately I think the only thing my patch does is make the brute-force scenario harder (which is already very difficult to do).

Also, aside from replay attacks, someone could currently write their own remember-me cookies (pretend to be any user) if they got a hold of your database. This patch would prevent that situation too (as long as they don't have your secret token).

@tiegz tiegz referenced this pull request
Open

Remember me & timeout #308

@binarylogic
Owner

I like it, merged

@binarylogic binarylogic merged commit 881102d into from
@tiegz
Collaborator

@yourewelcome thanks! Fwiw, I recently noticed that Devise uses signed cookies too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
View
40 lib/authlogic/session/cookies.rb
@@ -65,6 +65,15 @@ def httponly(value = nil)
rw_config(:httponly, value, false)
end
alias_method :httponly=, :httponly
+
+ # Should the cookie be signed? If the controller adapter supports it, this is a measure against cookie tampering.
+ def sign_cookie(value = nil)
+ if value && !controller.cookies.respond_to?(:signed)
+ raise "Signed cookies not supported with #{controller.class}!"
+ end
+ rw_config(:sign_cookie, value, false)
+ end
+ alias_method :sign_cookie=, :sign_cookie
end
# The methods available for an Authlogic::Session::Base object that make up the cookie feature set.
@@ -148,13 +157,33 @@ def httponly?
httponly == true || httponly == "true" || httponly == "1"
end
+ # If the cookie should be signed
+ def sign_cookie
+ return @sign_cookie if defined?(@sign_cookie)
+ @sign_cookie = self.class.sign_cookie
+ end
+
+ # Accepts a boolean as to whether the cookie should be signed. If true the cookie will be saved and verified using a signature.
+ def sign_cookie=(value)
+ @sign_cookie = value
+ end
+
+ # See sign_cookie
+ def sign_cookie?
+ sign_cookie == true || sign_cookie == "true" || sign_cookie == "1"
+ end
+
private
def cookie_key
build_key(self.class.cookie_key)
end
def cookie_credentials
- controller.cookies[cookie_key] && controller.cookies[cookie_key].split("::")
+ if self.class.sign_cookie
+ controller.cookies.signed[cookie_key] && controller.cookies.signed[cookie_key].split("::")
+ else
+ controller.cookies[cookie_key] && controller.cookies[cookie_key].split("::")
+ end
end
# Tries to validate the session from information in the cookie
@@ -171,13 +200,20 @@ def persist_by_cookie
def save_cookie
remember_me_until_value = "::#{remember_me_until}" if remember_me?
- controller.cookies[cookie_key] = {
+
+ cookie = {
:value => "#{record.persistence_token}::#{record.send(record.class.primary_key)}#{remember_me_until_value}",
:expires => remember_me_until,
:secure => secure,
:httponly => httponly,
:domain => controller.cookie_domain
}
+
+ if sign_cookie?
+ controller.cookies.signed[cookie_key] = cookie
+ else
+ controller.cookies[cookie_key] = cookie
+ end
end
def destroy_cookie
View
25 lib/authlogic/test_case/mock_cookie_jar.rb
@@ -9,6 +9,31 @@ def [](key)
def delete(key, options = {})
super(key)
end
+
+ def signed
+ @signed ||= MockSignedCookieJar.new(self)
+ end
+ end
+
+ class MockSignedCookieJar < MockCookieJar
+ attr_reader :parent_jar # helper for testing
+
+ def initialize(parent_jar)
+ @parent_jar = parent_jar
+ end
+
+ def [](val)
+ if signed_message = @parent_jar[val]
+ payload, signature = signed_message.split('--')
+ raise "Invalid signature" unless Digest::SHA1.hexdigest(payload) == signature
+ payload
+ end
+ end
+
+ def []=(key, options)
+ options[:value] = "#{options[:value]}--#{Digest::SHA1.hexdigest options[:value]}"
+ @parent_jar[key] = options
+ end
end
end
end
View
25 test/session_test/cookies_test.rb
@@ -65,6 +65,18 @@ def test_httponly
session = UserSession.new
assert_equal false, session.httponly
end
+
+ def test_sign_cookie
+ UserSession.sign_cookie = true
+ assert_equal true, UserSession.sign_cookie
+ session = UserSession.new
+ assert_equal true, session.sign_cookie
+
+ UserSession.sign_cookie false
+ assert_equal false, UserSession.sign_cookie
+ session = UserSession.new
+ assert_equal false, session.sign_cookie
+ end
end
class InstanceMethodsTest < ActiveSupport::TestCase
@@ -136,6 +148,19 @@ def test_after_save_save_cookie
assert_equal "#{ben.persistence_token}::#{ben.id}", controller.cookies["user_credentials"]
end
+ def test_after_save_save_cookie_signed
+ ben = users(:ben)
+
+ assert_nil controller.cookies["user_credentials"]
+ payload = "#{ben.persistence_token}::#{ben.id}"
+
+ session = UserSession.new(ben)
+ session.sign_cookie = true
+ assert session.save
+ assert_equal payload, controller.cookies.signed["user_credentials"]
+ assert_equal "#{payload}--#{Digest::SHA1.hexdigest payload}", controller.cookies.signed.parent_jar["user_credentials"]
+ end
+
def test_after_save_save_cookie_with_remember_me
ben = users(:ben)
session = UserSession.new(ben)
Something went wrong with that request. Please try again.