Skip to content
Browse files

Merge pull request #177 from thoughtbot/rack

Perform authentication in Rack middleware
  • Loading branch information...
2 parents f37add6 + e18fee0 commit 498bff3ae1825730a8c5dbd116c255cd42f6d92e @croaky croaky committed
View
9 Gemfile.lock
@@ -54,6 +54,8 @@ GEM
rspec (>= 2.6.0)
bcat (0.6.1)
rack (~> 1.0)
+ bourne (1.0)
+ mocha (= 0.9.8)
builder (2.1.2)
capybara (1.0.1)
mime-types (>= 1.16)
@@ -99,7 +101,8 @@ GEM
mime-types (~> 1.16)
treetop (~> 1.4.8)
mime-types (1.16)
- mocha (0.9.12)
+ mocha (0.9.8)
+ rake
nokogiri (1.5.0)
polyglot (0.3.2)
rack (1.2.3)
@@ -147,6 +150,7 @@ GEM
sqlite3 (1.3.4)
term-ansicolor (1.0.6)
thor (0.14.6)
+ timecop (0.3.5)
treetop (1.4.10)
polyglot
polyglot (>= 0.3.1)
@@ -160,6 +164,7 @@ PLATFORMS
DEPENDENCIES
appraisal (~> 0.3.8)
aruba (~> 0.4.2)
+ bourne
bundler (~> 1.0.0)
capybara (~> 1.0.0)
clearance!
@@ -167,7 +172,7 @@ DEPENDENCIES
database_cleaner
factory_girl_rails
launchy
- mocha
rspec-rails (~> 2.6.0)
shoulda-matchers!
sqlite3
+ timecop
View
32 README.md
@@ -86,6 +86,26 @@ Clearance will deliver one email on your app's behalf: when a user resets their
config.mailer_sender = "me@example.com"
end
+Rack
+----
+
+Clearance adds its session to the Rack environment hash so middleware and other
+Rack applications can interact with it:
+
+ class Bubblegum::Middleware
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ if env[:clearance].signed_in?
+ env[:clearance].current_user.bubble_gum
+ end
+ @app.call(env)
+ end
+ end
+
+
Overriding defaults
-------------------
@@ -214,16 +234,18 @@ Then run your tests!
rake
-Optional test matchers
-----------------------
+Testing
+-------
-Clearance comes with test matchers that are compatible with RSpec and Test::Unit.
+If you want to write Rails functional tests or controller specs with Clearance,
+you'll need to require the included test helpers and matchers.
-To use them, require the test matchers. For example, in spec/support/clearance.rb:
+For example, in spec/support/clearance.rb or test/test_helper.rb:
require 'clearance/testing'
-You'll then have access to methods like:
+This will make Clearance::Authentication methods work in your controllers
+during functional tests and provide access to helper methods like:
sign_in
sign_in_as(user)
View
3 clearance.gemspec
@@ -30,7 +30,8 @@ Gem::Specification.new do |s|
s.add_development_dependency('cucumber-rails', '~> 1.0.2')
s.add_development_dependency('rspec-rails', '~> 2.6.0')
s.add_development_dependency('sqlite3')
- s.add_development_dependency('mocha')
+ s.add_development_dependency('bourne')
+ s.add_development_dependency('timecop')
if s.respond_to? :specification_version then
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
View
1 features/support/env.rb
@@ -53,4 +53,3 @@
rescue NameError
raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it."
end
-
View
4 gemfiles/3.0.9.gemfile.lock
@@ -5,9 +5,9 @@ GIT
shoulda-matchers (1.0.0.beta3)
PATH
- remote: /Users/croaky/dev/clearance
+ remote: /Users/jferris/Source/clearance
specs:
- clearance (0.12.0)
+ clearance (0.13.0)
diesel (~> 0.1.5)
rails (>= 3.0)
View
6 gemfiles/3.1.0.gemfile.lock
@@ -5,9 +5,9 @@ GIT
shoulda-matchers (1.0.0.beta3)
PATH
- remote: /Users/croaky/dev/clearance
+ remote: /Users/jferris/Source/clearance
specs:
- clearance (0.12.0)
+ clearance (0.13.0)
diesel (~> 0.1.5)
rails (>= 3.0)
@@ -154,7 +154,7 @@ GEM
sprockets (2.0.0)
hike (~> 1.2)
rack (~> 1.0)
- tilt (!= 1.3.0, ~> 1.1)
+ tilt (~> 1.1, != 1.3.0)
spruz (0.2.13)
sqlite3 (1.3.4)
term-ansicolor (1.0.6)
View
4 lib/clearance.rb
@@ -1,5 +1,7 @@
require 'clearance/configuration'
+require 'clearance/session'
+require 'clearance/rack_session'
require 'clearance/authentication'
require 'clearance/user'
require 'clearance/engine'
-require 'clearance/password_strategies'
+require 'clearance/password_strategies'
View
28 lib/clearance/authentication.rb
@@ -10,32 +10,32 @@ module Authentication
:authorize, :deny_access
end
- # User in the current cookie
+ # Finds the user from the rack clearance session
#
# @return [User, nil]
def current_user
- @_current_user ||= user_from_cookie
+ clearance_session.current_user
end
# Set the current user
#
# @param [User]
def current_user=(user)
- @_current_user = user
+ clearance_session.sign_in user
end
# Is the current user signed in?
#
# @return [true, false]
def signed_in?
- ! current_user.nil?
+ clearance_session.signed_in?
end
# Is the current user signed out?
#
# @return [true, false]
def signed_out?
- current_user.nil?
+ !signed_in?
end
# Sign user in to cookie.
@@ -45,13 +45,7 @@ def signed_out?
# @example
# sign_in(@user)
def sign_in(user)
- if user
- cookies[:remember_token] = {
- :value => user.remember_token,
- :expires => Clearance.configuration.cookie_expiration.call
- }
- self.current_user = user
- end
+ clearance_session.sign_in user
end
# Sign user out of cookie.
@@ -59,9 +53,7 @@ def sign_in(user)
# @example
# sign_out
def sign_out
- current_user.reset_remember_token! if current_user
- cookies.delete(:remember_token)
- self.current_user = nil
+ clearance_session.sign_out
end
# Find the user by the given params or return nil.
@@ -107,10 +99,8 @@ def handle_unverified_request
protected
- def user_from_cookie
- if token = cookies[:remember_token]
- ::User.find_by_remember_token(token)
- end
+ def clearance_session
+ request.env[:clearance]
end
def store_location
View
2 lib/clearance/engine.rb
@@ -6,5 +6,7 @@ class Engine < Rails::Engine
initializer "clearance.filter" do |app|
app.config.filter_parameters += [:token, :password]
end
+
+ config.app_middleware.insert_after ActionDispatch::Cookies, Clearance::RackSession
end
end
View
15 lib/clearance/rack_session.rb
@@ -0,0 +1,15 @@
+module Clearance
+ class RackSession
+ def initialize(app)
+ @app = app
+ end
+
+ def call(env)
+ session = Clearance::Session.new(env)
+ env_with_clearance = env.merge(:clearance => session)
+ response = @app.call(env_with_clearance)
+ session.add_cookie_to_headers response[1]
+ response
+ end
+ end
+end
View
55 lib/clearance/session.rb
@@ -0,0 +1,55 @@
+module Clearance
+ class Session
+ REMEMBER_TOKEN_COOKIE = "remember_token".freeze
+
+ def initialize(env)
+ @env = env
+ end
+
+ def signed_in?
+ current_user.present?
+ end
+
+ def current_user
+ @current_user ||= with_remember_token do |token|
+ ::User.find_by_remember_token(token)
+ end
+ end
+
+ def sign_in(user)
+ @current_user = user
+ end
+
+ def sign_out
+ current_user.reset_remember_token! if signed_in?
+ @current_user = nil
+ cookies.delete(REMEMBER_TOKEN_COOKIE)
+ end
+
+ def add_cookie_to_headers(headers)
+ if signed_in?
+ Rack::Utils.set_cookie_header!(headers,
+ REMEMBER_TOKEN_COOKIE,
+ :value => current_user.remember_token,
+ :expires => Clearance.configuration.cookie_expiration.call,
+ :path => "/")
+ end
+ end
+
+ private
+
+ def with_remember_token
+ if token = remember_token
+ yield token
+ end
+ end
+
+ def remember_token
+ cookies[REMEMBER_TOKEN_COOKIE]
+ end
+
+ def cookies
+ @cookies ||= Rack::Request.new(@env).cookies
+ end
+ end
+end
View
5 lib/clearance/testing/helpers.rb
@@ -13,6 +13,11 @@ def sign_in
def sign_out
@controller.current_user = nil
end
+
+ def setup_controller_request_and_response
+ super
+ @request.env[:clearance] = Clearance::Session.new(@request.env)
+ end
end
end
end
View
23 spec/clearance/rack_session_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe Clearance::RackSession do
+ it "injects a clearance session into the environment" do
+ expected_session = "the session"
+ expected_session.stubs(:add_cookie_to_headers)
+ Clearance::Session.stubs(:new => expected_session)
+ headers = { "X-Roaring-Lobster" => "Red" }
+
+ app = Rack::Builder.new do
+ use Clearance::RackSession
+ run lambda { |env| Rack::Response.new(env[:clearance], 200, headers).finish }
+ end
+
+ env = Rack::MockRequest.env_for("/")
+
+ response = Rack::MockResponse.new(*app.call(env))
+
+ Clearance::Session.should have_received(:new).with(env)
+ response.body.should == expected_session
+ expected_session.should have_received(:add_cookie_to_headers).with(has_entries(headers))
+ end
+end
View
109 spec/clearance/session_spec.rb
@@ -0,0 +1,109 @@
+require 'spec_helper'
+
+describe Clearance::Session do
+ before { Timecop.freeze }
+ after { Timecop.return }
+
+ it "finds a user from a cookie" do
+ user = Factory(:user)
+ env = env_with_remember_token(user.remember_token)
+
+ session = Clearance::Session.new(env)
+ session.should be_signed_in
+ session.current_user.should == user
+ end
+
+ it "returns nil for an unknown user" do
+ user = Factory(:user)
+ env = env_with_remember_token("bogus")
+
+ session = Clearance::Session.new(env)
+ session.should_not be_signed_in
+ session.current_user.should be_nil
+ end
+
+ it "returns nil without a remember token" do
+ env = env_without_remember_token
+ session = Clearance::Session.new(env)
+ session.should_not be_signed_in
+ session.current_user.should be_nil
+ end
+
+ it "signs in a given user" do
+ user = Factory(:user)
+ session = Clearance::Session.new(env_without_remember_token)
+ session.sign_in user
+ session.current_user.should == user
+ end
+
+ it "sets a remember token cookie with a default expiration of 1 year from now" do
+ user = Factory(:user)
+ headers = {}
+ session = Clearance::Session.new(env_without_remember_token)
+ session.sign_in user
+ session.add_cookie_to_headers headers
+ headers.should set_cookie("remember_token", user.remember_token, 1.year.from_now)
+ end
+
+ it "sets a remember token cookie with a custom expiration" do
+ custom_expiration = 1.day.from_now
+ with_custom_expiration 1.day.from_now do
+ user = Factory(:user)
+ headers = {}
+ session = Clearance::Session.new(env_without_remember_token)
+ session.sign_in user
+ session.add_cookie_to_headers headers
+ headers.should set_cookie("remember_token", user.remember_token, 1.day.from_now)
+ Clearance.configuration.cookie_expiration.call.should be_within(100).of(1.year.from_now)
+ end
+ end
+
+ it "doesn't set a remember token when signed out" do
+ headers = {}
+ session = Clearance::Session.new(env_without_remember_token)
+ session.add_cookie_to_headers headers
+ headers.should_not set_cookie("remember_token")
+ end
+
+ it "signs out a user" do
+ user = Factory(:user)
+ old_remember_token = user.remember_token
+ env = env_with_remember_token(old_remember_token)
+
+ session = Clearance::Session.new(env)
+ session.sign_out
+ session.current_user.should be_nil
+ user.reload.remember_token.should_not == old_remember_token
+ end
+
+ def env_with_remember_token(token)
+ env_with_cookies("remember_token" => token)
+ end
+
+ def env_without_remember_token
+ env_with_cookies({})
+ end
+
+ def env_with_cookies(cookies)
+ Rack::MockRequest.env_for("/", "HTTP_COOKIE" => serialize_cookies(cookies))
+ end
+
+ def serialize_cookies(hash)
+ header = {}
+ hash.each do |key, value|
+ Rack::Utils.set_cookie_header!(header, key, value)
+ end
+ header['Set-Cookie']
+ end
+
+ def with_custom_expiration(custom_duration)
+ Clearance.configuration.cookie_expiration = lambda { custom_duration }
+ ensure
+ restore_default_config
+ end
+
+ def restore_default_config
+ Clearance.configuration = nil
+ Clearance.configure {}
+ end
+end
View
56 spec/controllers/sessions_controller_spec.rb
@@ -20,12 +20,8 @@
it { should redirect_to_url_after_create }
- it "sets a remember token cookie" do
- should set_cookie("remember_token", "old-token", Clearance.configuration.cookie_expiration.call)
- end
-
- it "should have a default of 1 year from now" do
- Clearance.configuration.cookie_expiration.call.should be_within(100).of(1.year.from_now)
+ it "sets the user in the clearance session" do
+ controller.current_user.should == @user
end
it "should not change the remember token" do
@@ -33,50 +29,6 @@
end
end
- describe "on POST to #create with good credentials - cookie duration set to 2 weeks" do
- custom_duration = 2.weeks.from_now.utc
-
- before do
- Clearance.configuration.cookie_expiration = lambda { custom_duration }
- @user = Factory(:user)
- @user.update_attribute(:remember_token, "old-token2")
- post :create, :session => {
- :email => @user.email,
- :password => @user.password }
- end
-
- it "sets a remember token cookie" do
- should set_cookie("remember_token", "old-token2", custom_duration)
- end
-
- after do
- # restore default Clearance configuration
- Clearance.configuration = nil
- Clearance.configure {}
- end
- end
-
- describe "on POST to #create with good credentials - cookie expiration set to nil (session cookie)" do
- before do
- Clearance.configuration.cookie_expiration = lambda { nil }
- @user = Factory(:user)
- @user.update_attribute(:remember_token, "old-token3")
- post :create, :session => {
- :email => @user.email,
- :password => @user.password }
- end
-
- it "unsets a remember token cookie" do
- should set_cookie("remember_token", "old-token3", nil)
- end
-
- after do
- # restore default Clearance configuration
- Clearance.configuration = nil
- Clearance.configure {}
- end
- end
-
describe "on POST to #create with good credentials and a session return url" do
before do
@user = Factory(:user)
@@ -141,10 +93,6 @@
it { should redirect_to_url_after_destroy }
- it "should delete the cookie token" do
- cookies['remember_token'].should be_nil
- end
-
it "should reset the remember token" do
@user.reload.remember_token.should_not == "old-token"
end
View
2 spec/spec_helper.rb
@@ -10,6 +10,8 @@
require 'diesel/testing'
require 'rspec/rails'
+require 'bourne'
+require 'timecop'
require 'clearance/testing'
View
56 spec/support/cookies.rb
@@ -1,27 +1,29 @@
-RSpec::Matchers.define :set_cookie do |name, value, expected_expires_at|
+RSpec::Matchers.define :set_cookie do |name, expected_value, expected_expires_at|
match do |subject|
- @response = subject.response
- @name = name
- @value = value
+ @headers = subject
+ @expected_name = name
+ @expected_value = expected_value
@expected_expires_at = expected_expires_at
extract_cookies
find_expected_cookie
parse_expiration
+ parse_value
+ parse_path
ensure_cookie_set
- ensure_value_correct
ensure_expiration_correct
+ ensure_path_is_correct
end
def extract_cookies
- @cookie_headers = @response.headers['Set-Cookie'] || []
+ @cookie_headers = @headers['Set-Cookie'] || []
@cookie_headers = [@cookie_headers] if @cookie_headers.respond_to?(:to_str)
end
def find_expected_cookie
@cookie = @cookie_headers.detect do |header|
- header =~ /^#{@name}=[^;]*(;|$)/
+ header =~ /^#{@expected_name}=[^;]*(;|$)/
end
end
@@ -31,21 +33,29 @@ def parse_expiration
end
end
- def ensure_cookie_set
- @cookie.should_not be_nil
+ def parse_value
+ if @cookie && result = @cookie.match(/=(.*?)(?:;|$)/)
+ @value = result[1]
+ end
+ end
+
+ def parse_path
+ if @cookie && result = @cookie.match(/; path=(.*?)(;|$)/)
+ @path = result[1]
+ end
end
- def ensure_value_correct
- @response.cookies[@name].should == @value
+ def ensure_cookie_set
+ @value.should == @expected_value
end
def ensure_expiration_correct
- if @expected_expires_at
- @expires_at.should_not be_nil
- @expires_at.should be_within(100).of(@expected_expires_at)
- else
- @expires_at.should be_nil
- end
+ @expires_at.should_not be_nil
+ @expires_at.should be_within(100).of(@expected_expires_at)
+ end
+
+ def ensure_path_is_correct
+ @path.should == "/"
end
failure_message do
@@ -53,20 +63,14 @@ def ensure_expiration_correct
end
def expectation
- base = "Expected a cookie named #{@name} with value #{@value.inspect} "
- if @expected_expires_at
- base << "expiring at #{@expected_expires_at.inspect}"
- else
- base << "with no expiration"
- end
- base
+ "a cookie named #{@expected_name} with value #{@expected_value.inspect} expiring at #{@expected_expires_at.inspect}"
end
def result
if @cookie
- "value #{@value.inspect} expiring #{@expires_at.inspect}"
+ @cookie
else
- "cookies #{@response.cookies.inspect}"
+ @cookie_headers.join("; ")
end
end
end

0 comments on commit 498bff3

Please sign in to comment.
Something went wrong with that request. Please try again.