Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added cookies.permanent, cookies.signed, and cookies.permanent.signed…

… accessor for common cookie actions [DHH]
  • Loading branch information...
commit 0200e20f148c96afceeebc4da7b5985643f9f707 1 parent e4ebaab
@dhh dhh authored
View
19 actionpack/CHANGELOG
@@ -1,3 +1,22 @@
+*Edge*
+
+* Added cookies.permanent, cookies.signed, and cookies.permanent.signed accessor for common cookie actions [DHH]. Examples:
+
+ cookies.permanent[:prefers_open_id] = true
+ # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+
+ cookies.signed[:discount] = 45
+ # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
+
+ cookies.signed[:discount]
+ # => 45 (if the cookie was changed, you'll get a InvalidSignature exception)
+
+ cookies.permanent.signed[:remember_me] = current_user.id
+ # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+
+ ...to use the signed cookies, you need to set a secret to ActionController::Base.cookie_verifier_secret (automatically done in config/initializers/cookie_verification_secret.rb for new Rails applications).
+
+
*2.3.5 (November 25, 2009)*
* Minor Bug Fixes and deprecation warnings
View
94 actionpack/lib/action_controller/cookies.rb
@@ -46,6 +46,7 @@ module ActionController #:nodoc:
module Cookies
def self.included(base)
base.helper_method :cookies
+ base.cattr_accessor :cookie_verifier_secret
end
protected
@@ -56,6 +57,8 @@ def cookies
end
class CookieJar < Hash #:nodoc:
+ attr_reader :controller
+
def initialize(controller)
@controller, @cookies = controller, controller.request.cookies
super()
@@ -91,5 +94,96 @@ def delete(key, options = {})
@controller.response.delete_cookie(key, options)
value
end
+
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
+ #
+ # cookies.permanent[:prefers_open_id] = true
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ #
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
+ #
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
+ #
+ # cookies.permanent.signed[:remember_me] = current_user.id
+ # # => Set-Cookie: discount=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ def permanent
+ @permanent ||= PermanentCookieJar.new(self)
+ end
+
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
+ # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
+ # be raised.
+ #
+ # This jar requires that you set a suitable secret for the verification on ActionController::Base.cookie_verifier_secret.
+ #
+ # Example:
+ #
+ # cookies.signed[:discount] = 45
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
+ #
+ # cookies.signed[:discount] # => 45
+ def signed
+ @signed ||= SignedCookieJar.new(self)
+ end
+ end
+
+ class PermanentCookieJar < CookieJar #:nodoc:
+ def initialize(parent_jar)
+ @parent_jar = parent_jar
+ end
+
+ def []=(key, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ else
+ options = { :value => options }
+ end
+
+ options[:expires] = 20.years.from_now
+ @parent_jar[key] = options
+ end
+
+ def signed
+ @signed ||= SignedCookieJar.new(self)
+ end
+
+ def controller
+ @parent_jar.controller
+ end
+
+ def method_missing(method, *arguments, &block)
+ @parent_jar.send(method, *arguments, &block)
+ end
+ end
+
+ class SignedCookieJar < CookieJar #:nodoc:
+ def initialize(parent_jar)
+ unless parent_jar.controller.class.cookie_verifier_secret
+ raise "You must set ActionController::Base.cookie_verifier_secret to use signed cookies"
+ end
+
+ @parent_jar = parent_jar
+ @verifier = ActiveSupport::MessageVerifier.new(@parent_jar.controller.class.cookie_verifier_secret)
+ end
+
+ def [](name)
+ @verifier.verify(@parent_jar[name])
+ end
+
+ def []=(key, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ options[:value] = @verifier.generate(options[:value])
+ else
+ options = { :value => @verifier.generate(options) }
+ end
+
+ @parent_jar[key] = options
+ end
+
+ def method_missing(method, *arguments, &block)
+ @parent_jar.send(method, *arguments, &block)
+ end
end
end
View
33 actionpack/test/controller/cookie_test.rb
@@ -2,6 +2,8 @@
class CookieTest < ActionController::TestCase
class TestController < ActionController::Base
+ self.cookie_verifier_secret = "thisISverySECRET123"
+
def authenticate
cookies["user_name"] = "david"
end
@@ -39,6 +41,18 @@ def delete_cookie_with_path
def authenticate_with_http_only
cookies["user_name"] = { :value => "david", :httponly => true }
end
+
+ def set_permanent_cookie
+ cookies.permanent[:user_name] = "Jamie"
+ end
+
+ def set_signed_cookie
+ cookies.signed[:user_id] = 45
+ end
+
+ def set_permanent_signed_cookie
+ cookies.permanent.signed[:remember_me] = 100
+ end
def rescue_action(e)
raise unless ActionView::MissingTemplate # No templates here, and we don't care about the output
@@ -131,4 +145,21 @@ def test_cookies_persist_throughout_request
cookies = @controller.send(:cookies)
assert_equal 'david', cookies['user_name']
end
-end
+
+ def test_permanent_cookie
+ get :set_permanent_cookie
+ assert_match /Jamie/, @response.headers["Set-Cookie"].first
+ assert_match %r(#{20.years.from_now.year}), @response.headers["Set-Cookie"].first
+ end
+
+ def test_signed_cookie
+ get :set_signed_cookie
+ assert_equal 45, @controller.send(:cookies).signed[:user_id]
+ end
+
+ def test_permanent_signed_cookie
+ get :set_permanent_signed_cookie
+ assert_match %r(#{20.years.from_now.year}), @response.headers["Set-Cookie"].first
+ assert_equal 100, @controller.send(:cookies).signed[:remember_me]
+ end
+end
View
3  railties/CHANGELOG
@@ -1,7 +1,10 @@
*Edge*
+* Added config/initializers/cookie_verification_secret.rb with an auto-generated secret for using ActionController::Base#cookies.signed [DHH]
+
* Fixed that the debugger wouldn't go into IRB mode because of left-over ARGVs [DHH]
+
* 1.9 compatibility
*2.3.4 (September 4, 2009)*
View
7 railties/configs/initializers/cookie_verification_secret.rb
@@ -0,0 +1,7 @@
+# Be sure to restart your server when you modify this file.
+
+# Your secret key for verifying the integrity of signed cookies.
+# If you change this key, all old signed cookies will become invalid!
+# Make sure the secret is at least 30 characters and all random,
+# no regular words or you'll be exposed to dictionary attacks.
+ActionController::Base.cookie_verification_secret = '<%= app_secret %>';
View
3  railties/lib/rails_generator/generators/applications/app/app_generator.rb
@@ -193,6 +193,9 @@ def create_initializer_files(m)
m.template "configs/initializers/session_store.rb", "config/initializers/session_store.rb",
:assigns => { :app_name => @app_name, :app_secret => ActiveSupport::SecureRandom.hex(64) }
+
+ m.template "configs/initializers/cookie_verification_secret.rb",
+ :assigns => { :app_secret => ActiveSupport::SecureRandom.hex(64) }
end
def create_locale_file(m)
Please sign in to comment.
Something went wrong with that request. Please try again.