├── init.rb ├── cucumber.yml ├── .rspec ├── Gemfile ├── lib ├── clearance │ ├── version.rb │ ├── password_strategies.rb │ ├── testing │ │ ├── assertion_error.rb │ │ ├── helpers.rb │ │ └── deny_access_matcher.rb │ ├── engine.rb │ ├── rack_session.rb │ ├── testing.rb │ ├── configuration.rb │ ├── password_strategies │ │ ├── blowfish.rb │ │ └── sha1.rb │ ├── session.rb │ ├── authentication.rb │ └── user.rb ├── generators │ └── clearance │ │ ├── install │ │ ├── templates │ │ │ ├── user.rb │ │ │ ├── clearance.rb │ │ │ ├── README │ │ │ └── db │ │ │ │ └── migrate │ │ │ │ └── upgrade_clearance_to_diesel.rb │ │ └── install_generator.rb │ │ ├── views │ │ └── views_generator.rb │ │ └── features │ │ └── features_generator.rb └── clearance.rb ├── features ├── support │ ├── aruba.rb │ ├── clearance.rb │ └── env.rb ├── engine │ ├── visitor_signs_out.feature │ ├── visitor_signs_up.feature │ ├── visitor_signs_in.feature │ └── visitor_resets_password.feature ├── step_definitions │ ├── configuration_steps.rb │ └── engine │ │ └── clearance_steps.rb ├── integration_with_test_unit.feature └── integration.feature ├── .gitignore ├── Appraisals ├── gemfiles ├── 3.1.4.gemfile ├── 3.2.3.gemfile ├── 3.0.12.gemfile ├── 3.0.12.gemfile.lock ├── 3.2.3.gemfile.lock └── 3.1.4.gemfile.lock ├── app ├── views │ ├── passwords │ │ ├── create.html.erb │ │ ├── new.html.erb │ │ └── edit.html.erb │ ├── users │ │ ├── new.html.erb │ │ └── _form.html.erb │ ├── sessions │ │ ├── new.html.erb │ │ └── _form.html.erb │ ├── clearance_mailer │ │ └── change_password.html.erb │ └── layouts │ │ └── application.html.erb ├── mailers │ └── clearance_mailer.rb └── controllers │ └── clearance │ ├── sessions_controller.rb │ ├── users_controller.rb │ └── passwords_controller.rb ├── spec ├── factories.rb ├── spec_helper.rb ├── controllers │ ├── flashes_controller_spec.rb │ ├── denies_controller_spec.rb │ ├── forgeries_controller_spec.rb │ ├── users_controller_spec.rb │ ├── sessions_controller_spec.rb │ └── passwords_controller_spec.rb ├── configuration_spec.rb ├── mailers │ └── clearance_mailer_spec.rb ├── clearance │ ├── rack_session_spec.rb │ └── session_spec.rb ├── support │ ├── clearance.rb │ └── cookies.rb └── models │ ├── clearance_user_spec.rb │ ├── sha1_spec.rb │ ├── blowfish_spec.rb │ └── user_spec.rb ├── .travis.yml ├── db ├── migrate │ └── 20110111224543_create_diesel_clearance_users.rb └── schema.rb ├── Rakefile ├── config └── routes.rb ├── LICENSE ├── CONTRIBUTING.md ├── clearance.gemspec ├── Gemfile.lock ├── README.md └── NEWS.md /init.rb: -------------------------------------------------------------------------------- 1 | require 'clearance' -------------------------------------------------------------------------------- /cucumber.yml: -------------------------------------------------------------------------------- 1 | default: -r features 2 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /lib/clearance/version.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | VERSION = '0.16.2' 3 | end 4 | -------------------------------------------------------------------------------- /features/support/aruba.rb: -------------------------------------------------------------------------------- 1 | Before do 2 | @aruba_timeout_seconds = 60 3 | end 4 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include Clearance::User 3 | end 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | *.swp 3 | *.swo 4 | *~ 5 | !.keep 6 | .bundle 7 | db/*.sqlite3 8 | log/*.log 9 | pkg 10 | tmp/ 11 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/clearance.rb: -------------------------------------------------------------------------------- 1 | Clearance.configure do |config| 2 | config.mailer_sender = 'reply@example.com' 3 | end 4 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | ['3.2.3', '3.1.4', '3.0.12'].each do |rails_version| 2 | appraise "#{rails_version}" do 3 | gem "rails", rails_version 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /gemfiles/3.1.4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "3.1.4" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/3.2.3.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "3.2.3" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /gemfiles/3.0.12.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "http://rubygems.org" 4 | 5 | gem "rails", "3.0.12" 6 | 7 | gemspec :path=>"../" -------------------------------------------------------------------------------- /app/views/passwords/create.html.erb: -------------------------------------------------------------------------------- 1 |

2 | You will receive an email within the next few minutes. 3 | It contains instructions for changing your password. 4 |

5 | -------------------------------------------------------------------------------- /app/views/users/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign up

2 | 3 | <%= form_for @user do |form| %> 4 | <%= render :partial => '/users/form', :object => form %> 5 | <%= form.submit 'Sign up' %> 6 | <% end %> 7 | -------------------------------------------------------------------------------- /spec/factories.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | 3 | sequence :email do |n| 4 | "user#{n}@example.com" 5 | end 6 | 7 | factory :user do 8 | email 9 | password "password" 10 | end 11 | 12 | end 13 | -------------------------------------------------------------------------------- /lib/generators/clearance/views/views_generator.rb: -------------------------------------------------------------------------------- 1 | require 'diesel/generators/views_base' 2 | 3 | module Clearance 4 | module Generators 5 | class ViewsGenerator < Diesel::Generators::ViewsBase 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module PasswordStrategies 3 | autoload :SHA1, 'clearance/password_strategies/sha1' 4 | autoload :Blowfish, 'clearance/password_strategies/blowfish' 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/generators/clearance/features/features_generator.rb: -------------------------------------------------------------------------------- 1 | require 'diesel/generators/features_base' 2 | 3 | module Clearance 4 | module Generators 5 | class FeaturesGenerator < Diesel::Generators::FeaturesBase 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /lib/clearance.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/configuration' 2 | require 'clearance/session' 3 | require 'clearance/rack_session' 4 | require 'clearance/authentication' 5 | require 'clearance/user' 6 | require 'clearance/engine' 7 | require 'clearance/password_strategies' 8 | -------------------------------------------------------------------------------- /app/views/users/_form.html.erb: -------------------------------------------------------------------------------- 1 |
2 | <%= form.label :email %> 3 | <%= form.text_field :email, :type => "email" %> 4 |
5 |
6 | <%= form.label :password %> 7 | <%= form.password_field :password %> 8 |
9 | -------------------------------------------------------------------------------- /app/views/sessions/new.html.erb: -------------------------------------------------------------------------------- 1 |

Sign in

2 | 3 | <%= render :partial => '/sessions/form' %> 4 | 5 | 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.2 4 | - 1.9.3 5 | before_install: 6 | - gem update --system 7 | before_script: 8 | - "bundle exec rake db:migrate" 9 | gemfile: 10 | - gemfiles/3.0.12.gemfile 11 | - gemfiles/3.1.4.gemfile 12 | - gemfiles/3.2.3.gemfile 13 | branches: 14 | only: 15 | - master 16 | -------------------------------------------------------------------------------- /lib/clearance/testing/assertion_error.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | if RUBY_VERSION > "1.9" 4 | require 'minitest/unit' 5 | AssertionError = MiniTest::Assertion 6 | else 7 | require 'test/unit/assertionfailederror' 8 | AssertionError = Test::Unit::AssertionFailedError 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/views/clearance_mailer/change_password.html.erb: -------------------------------------------------------------------------------- 1 | Someone, hopefully you, has requested that we send you a link to change your password. 2 | 3 | Here's the link: 4 | 5 | <%= edit_user_password_url(@user, :token => @user.confirmation_token.html_safe) %> 6 | 7 | If you didn't request this, ignore this email. Don't worry. Your password hasn't been changed. 8 | -------------------------------------------------------------------------------- /lib/clearance/engine.rb: -------------------------------------------------------------------------------- 1 | require "clearance" 2 | require "rails" 3 | 4 | module Clearance 5 | class Engine < Rails::Engine 6 | initializer "clearance.filter" do |app| 7 | app.config.filter_parameters += [:token, :password] 8 | end 9 | 10 | config.app_middleware.insert_after ActionDispatch::Cookies, Clearance::RackSession 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /features/support/clearance.rb: -------------------------------------------------------------------------------- 1 | require 'clearance' 2 | 3 | Clearance.configure do |config| 4 | end 5 | 6 | class ApplicationController < ActionController::Base 7 | include Clearance::Authentication 8 | 9 | def show 10 | render :text => "", :layout => 'application' 11 | end 12 | end 13 | 14 | class User < ActiveRecord::Base 15 | include Clearance::User 16 | end 17 | -------------------------------------------------------------------------------- /lib/clearance/rack_session.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | class RackSession 3 | def initialize(app) 4 | @app = app 5 | end 6 | 7 | def call(env) 8 | session = Clearance::Session.new(env) 9 | env[:clearance] = session 10 | response = @app.call(env) 11 | session.add_cookie_to_headers response[1] 12 | response 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /features/engine/visitor_signs_out.feature: -------------------------------------------------------------------------------- 1 | Feature: Sign out 2 | 3 | In order to protect my account from unauthorized access 4 | As a signed in user 5 | I want to sign out 6 | 7 | Scenario: User signs out 8 | Given I am signed up as "email@example.com" 9 | When I sign in as "email@example.com" 10 | Then I should be signed in 11 | When I sign out 12 | Then I should be signed out 13 | -------------------------------------------------------------------------------- /app/views/sessions/_form.html.erb: -------------------------------------------------------------------------------- 1 | <%= form_for :session, :url => session_path do |form| %> 2 |
3 | <%= form.label :email %> 4 | <%= form.text_field :email, :type => "email" %> 5 |
6 |
7 | <%= form.label :password %> 8 | <%= form.password_field :password %> 9 |
10 |
11 | <%= form.submit "Sign in" %> 12 |
13 | <% end %> 14 | -------------------------------------------------------------------------------- /app/mailers/clearance_mailer.rb: -------------------------------------------------------------------------------- 1 | class ClearanceMailer < ActionMailer::Base 2 | def change_password(user) 3 | @user = user 4 | mail :from => Clearance.configuration.mailer_sender, 5 | :to => @user.email, 6 | :subject => I18n.t(:change_password, 7 | :scope => [:clearance, :models, :clearance_mailer], 8 | :default => "Change your password") 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/passwords/new.html.erb: -------------------------------------------------------------------------------- 1 |

Reset your password

2 | 3 |

4 | We will email you a link to reset your password. 5 |

6 | 7 | <%= form_for :password, :url => passwords_path do |form| %> 8 |
9 | <%= form.label :email, "Email address" %> 10 | <%= form.text_field :email, :type => "email" %> 11 |
12 |
13 | <%= form.submit "Reset password" %> 14 |
15 | <% end %> 16 | -------------------------------------------------------------------------------- /app/views/passwords/edit.html.erb: -------------------------------------------------------------------------------- 1 |

Change your password

2 | 3 |

4 | Your password has been reset. Choose a new password below. 5 |

6 | 7 | <%= form_for(:user, 8 | :url => user_password_path(@user, :token => @user.confirmation_token), 9 | :html => { :method => :put }) do |form| %> 10 |
11 | <%= form.label :password, "Choose password" %> 12 | <%= form.password_field :password %> 13 |
14 |
15 | <%= form.submit "Save this password" %> 16 |
17 | <% end %> 18 | -------------------------------------------------------------------------------- /lib/clearance/testing/helpers.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | module Helpers 4 | def sign_in_as(user) 5 | @controller.current_user = user 6 | return user 7 | end 8 | 9 | def sign_in 10 | sign_in_as FactoryGirl.create(:user) 11 | end 12 | 13 | def sign_out 14 | @controller.current_user = nil 15 | end 16 | 17 | def setup_controller_request_and_response 18 | super 19 | @request.env[:clearance] = Clearance::Session.new(@request.env) 20 | end 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/clearance/testing.rb: -------------------------------------------------------------------------------- 1 | require 'clearance/testing/assertion_error' 2 | require 'clearance/testing/deny_access_matcher' 3 | require 'clearance/testing/helpers' 4 | 5 | if defined?(ActionController::TestCase) 6 | ActionController::TestCase.extend Clearance::Testing::Matchers 7 | class ActionController::TestCase 8 | include Clearance::Testing::Helpers 9 | end 10 | end 11 | 12 | if defined?(RSpec) && RSpec.respond_to?(:configure) 13 | RSpec.configure do |config| 14 | config.include Clearance::Testing::Matchers 15 | config.include Clearance::Testing::Helpers 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= javascript_include_tag :defaults %> 5 | <%= csrf_meta_tag %> 6 | 7 | 8 | 15 |
16 | <% flash.each do |key, value| -%> 17 |
<%=h value %>
18 | <% end %> 19 |
20 | <%= yield %> 21 | 22 | 23 | -------------------------------------------------------------------------------- /db/migrate/20110111224543_create_diesel_clearance_users.rb: -------------------------------------------------------------------------------- 1 | class CreateDieselClearanceUsers < ActiveRecord::Migration 2 | def self.up 3 | create_table(:users) do |t| 4 | t.string :email 5 | t.string :encrypted_password, :limit => 128 6 | t.string :salt, :limit => 128 7 | t.string :confirmation_token, :limit => 128 8 | t.string :remember_token, :limit => 128 9 | t.timestamps 10 | end 11 | 12 | add_index :users, :email 13 | add_index :users, :remember_token 14 | end 15 | 16 | def self.down 17 | drop_table :users 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /features/engine/visitor_signs_up.feature: -------------------------------------------------------------------------------- 1 | Feature: Sign up 2 | 3 | In order to access protected sections of the site 4 | As a visitor 5 | I want to sign up 6 | 7 | Scenario: Visitor signs up with invalid email 8 | When I sign up with "invalidemail" and "password" 9 | Then I am told to enter a valid email address 10 | 11 | Scenario: Visitor signs up with blank password 12 | When I sign up with "email@example.com" and "" 13 | Then I am told to enter a password 14 | 15 | Scenario: Visitor signs up with valid data 16 | When I sign up with "email@example.com" and "password" 17 | Then I should be signed in 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] ||= "test" 2 | 3 | PROJECT_ROOT = File.expand_path("../..", __FILE__) 4 | $LOAD_PATH << File.join(PROJECT_ROOT, "lib") 5 | 6 | require 'rails/all' 7 | require 'rails/test_help' 8 | 9 | Bundler.require 10 | 11 | require 'diesel/testing' 12 | require 'rspec/rails' 13 | require 'bourne' 14 | require 'timecop' 15 | require 'factory_girl_rails' 16 | require 'shoulda-matchers' 17 | 18 | require 'clearance/testing' 19 | 20 | Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} 21 | 22 | RSpec.configure do |config| 23 | config.mock_with :mocha 24 | config.use_transactional_fixtures = true 25 | config.include FactoryGirl::Syntax::Methods 26 | end 27 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | require 'bundler/gem_tasks' 6 | 7 | require 'rake' 8 | require 'diesel/tasks' 9 | require 'cucumber/rake/task' 10 | require 'rspec/core/rake_task' 11 | require 'appraisal' 12 | 13 | task :default do 14 | if ENV['BUNDLE_GEMFILE'] =~ /gemfiles/ 15 | exec 'rake spec cucumber' 16 | else 17 | Rake::Task['appraise'].execute 18 | end 19 | end 20 | 21 | task :appraise => ['appraisal:install'] do 22 | exec 'rake appraisal spec cucumber' 23 | end 24 | 25 | RSpec::Core::RakeTask.new(:spec) 26 | 27 | Cucumber::Rake::Task.new(:cucumber) do |t| 28 | t.fork = true 29 | t.cucumber_opts = ['--format', (ENV['CUCUMBER_FORMAT'] || 'progress')] 30 | end 31 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :passwords, 3 | :controller => 'clearance/passwords', 4 | :only => [:new, :create] 5 | 6 | resource :session, 7 | :controller => 'clearance/sessions', 8 | :only => [:new, :create, :destroy] 9 | 10 | resources :users, :controller => 'clearance/users', :only => [:new, :create] do 11 | resource :password, 12 | :controller => 'clearance/passwords', 13 | :only => [:create, :edit, :update] 14 | end 15 | 16 | match 'sign_up' => 'clearance/users#new', :as => 'sign_up' 17 | match 'sign_in' => 'clearance/sessions#new', :as => 'sign_in' 18 | match 'sign_out' => 'clearance/sessions#destroy', :via => :delete, :as => 'sign_out' 19 | end 20 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/README: -------------------------------------------------------------------------------- 1 | 2 | ******************************************************************************* 3 | 4 | Next steps: 5 | 6 | 1. Configure the mailer to create full URLs in emails: 7 | 8 | # config/environments/{development,test}.rb 9 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 10 | 11 | In production it should be your app's domain name. 12 | 13 | 2. Display flashes. For example, in your application layout: 14 | 15 | <% flash.each do |key, value| -%> 16 |
<%= value %>
17 | <% end -%> 18 | 19 | 3. Migrate: 20 | 21 | rake db:migrate 22 | 23 | ******************************************************************************* 24 | -------------------------------------------------------------------------------- /spec/controllers/flashes_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class FlashesController < ActionController::Base 4 | include Clearance::Authentication 5 | 6 | def set_flash 7 | flash[:notice] = params[:message] 8 | redirect_to view_flash_url 9 | end 10 | 11 | def view_flash 12 | render :text => "#{flash[:notice]}" 13 | end 14 | end 15 | 16 | describe FlashesController do 17 | before do 18 | Rails.application.routes.draw do 19 | match "set_flash" => "flashes#set_flash" 20 | match "view_flash" => "flashes#view_flash" 21 | end 22 | end 23 | 24 | after do 25 | Rails.application.reload_routes! 26 | end 27 | 28 | it "sets and views a flash" do 29 | visit "/set_flash?message=hello" 30 | page.should have_content("hello") 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /spec/configuration_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::Configuration do 4 | describe "when no user_model_name is specified" do 5 | before do 6 | Clearance.configure do |config| 7 | end 8 | end 9 | 10 | it "defaults to User" do 11 | Clearance.configuration.user_model.should == ::User 12 | end 13 | end 14 | 15 | describe "when a custom user_model_name is specified" do 16 | before do 17 | MyUser = Class.new 18 | Clearance.configure do |config| 19 | config.user_model = MyUser 20 | end 21 | end 22 | 23 | after do 24 | Clearance.configure do |config| 25 | config.user_model = ::User 26 | end 27 | end 28 | 29 | it "is used instead of User" do 30 | Clearance.configuration.user_model.should == ::MyUser 31 | end 32 | end 33 | end -------------------------------------------------------------------------------- /spec/mailers/clearance_mailer_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ClearanceMailer do 4 | before do 5 | @user = create(:user) 6 | @user.forgot_password! 7 | @email = ClearanceMailer.change_password(@user) 8 | end 9 | 10 | it "should be from DO_NOT_REPLY" do 11 | Clearance.configuration.mailer_sender.should =~ /#{@email.from[0]}/i 12 | end 13 | 14 | it "should be sent to user" do 15 | @email.to.first.should =~ /#{@user.email}/i 16 | end 17 | 18 | it "should contain a link to edit the user's password" do 19 | host = ActionMailer::Base.default_url_options[:host] 20 | regexp = %r{http://#{host}/users/#{@user.id}/password/edit\?token=#{@user.confirmation_token}} 21 | @email.body.to_s.should =~ regexp 22 | end 23 | 24 | it "should set its subject" do 25 | @email.subject.should =~ /Change your password/ 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /spec/clearance/rack_session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::RackSession do 4 | it "injects a clearance session into the environment" do 5 | expected_session = "the session" 6 | expected_session.stubs(:add_cookie_to_headers) 7 | Clearance::Session.stubs(:new => expected_session) 8 | headers = { "X-Roaring-Lobster" => "Red" } 9 | 10 | app = Rack::Builder.new do 11 | use Clearance::RackSession 12 | run lambda { |env| Rack::Response.new(env[:clearance], 200, headers).finish } 13 | end 14 | 15 | env = Rack::MockRequest.env_for("/") 16 | 17 | response = Rack::MockResponse.new(*app.call(env)) 18 | 19 | Clearance::Session.should have_received(:new).with(env) 20 | response.body.should == expected_session 21 | expected_session.should have_received(:add_cookie_to_headers).with(has_entries(headers)) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /features/engine/visitor_signs_in.feature: -------------------------------------------------------------------------------- 1 | Feature: Sign in 2 | 3 | In order to get access to protected sections of the site 4 | As a visitor 5 | I want to sign in 6 | 7 | Scenario: Visitor is not signed up 8 | When I sign in as "unknown.email@example.com" 9 | Then I am told email or password is bad 10 | And I should be signed out 11 | 12 | Scenario: Visitor enters wrong password 13 | Given I am signed up as "email@example.com" 14 | When I sign in as "email@example.com" and "badpassword" 15 | Then I am told email or password is bad 16 | And I should be signed out 17 | 18 | Scenario: Visitor signs in successfully 19 | Given I am signed up as "email@example.com" 20 | When I sign in as "email@example.com" 21 | Then I should be signed in 22 | 23 | Scenario: Visitor signs in successfully with uppercase email 24 | Given I am signed up as "email@example.com" 25 | When I sign in as "Email@example.com" 26 | Then I should be signed in 27 | -------------------------------------------------------------------------------- /spec/support/clearance.rb: -------------------------------------------------------------------------------- 1 | require 'clearance' 2 | 3 | Clearance.configure do |config| 4 | end 5 | 6 | class ApplicationController < ActionController::Base 7 | include Clearance::Authentication 8 | end 9 | 10 | class User < ActiveRecord::Base 11 | include Clearance::User 12 | end 13 | 14 | module Clearance 15 | module Test 16 | module Redirects 17 | def redirect_to_url_after_create 18 | redirect_to(@controller.send(:url_after_create)) 19 | end 20 | 21 | def redirect_to_url_after_update 22 | redirect_to(@controller.send(:url_after_update)) 23 | end 24 | 25 | def redirect_to_url_after_destroy 26 | redirect_to(@controller.send(:url_after_destroy)) 27 | end 28 | 29 | def redirect_to_url_already_confirmed 30 | redirect_to(@controller.send(:url_already_confirmed)) 31 | end 32 | end 33 | end 34 | end 35 | 36 | RSpec.configure do |config| 37 | config.include Clearance::Test::Redirects 38 | end 39 | -------------------------------------------------------------------------------- /app/controllers/clearance/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Clearance::SessionsController < ApplicationController 2 | unloadable 3 | 4 | skip_before_filter :authorize, :only => [:new, :create, :destroy] 5 | protect_from_forgery :except => :create 6 | 7 | def new 8 | render :template => 'sessions/new' 9 | end 10 | 11 | def create 12 | @user = authenticate(params) 13 | if @user.nil? 14 | flash_failure_after_create 15 | render :template => 'sessions/new', :status => :unauthorized 16 | else 17 | sign_in(@user) 18 | redirect_back_or(url_after_create) 19 | end 20 | end 21 | 22 | def destroy 23 | sign_out 24 | redirect_to(url_after_destroy) 25 | end 26 | 27 | private 28 | 29 | def flash_failure_after_create 30 | flash.now[:notice] = translate(:bad_email_or_password, 31 | :scope => [:clearance, :controllers, :sessions], 32 | :default => "Bad email or password.") 33 | end 34 | 35 | def url_after_create 36 | '/' 37 | end 38 | 39 | def url_after_destroy 40 | sign_in_url 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/models/clearance_user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::User do 4 | subject do 5 | Class.new do 6 | def self.validates_presence_of(*args); end 7 | def self.validates_uniqueness_of(*args); end 8 | def self.validates_format_of(*args); end 9 | def self.before_validation(*args); end 10 | def self.before_create(*args); end 11 | 12 | include Clearance::User 13 | end.new 14 | end 15 | 16 | describe "when Clearance.configuration.password_strategy is set" do 17 | let(:mock_password_strategy) { Module.new } 18 | 19 | before { Clearance.configuration.password_strategy = mock_password_strategy } 20 | 21 | it "includes the value it is set to" do 22 | subject.should be_kind_of(mock_password_strategy) 23 | end 24 | end 25 | 26 | describe "when Clearance.configuration.password_strategy is not set" do 27 | before { Clearance.configuration.password_strategy = nil } 28 | 29 | it "includes Clearance::PasswordStrategies::SHA1" do 30 | subject.should be_kind_of(Clearance::PasswordStrategies::SHA1) 31 | end 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2008-2011 thoughtbot, inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/clearance/configuration.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | class Configuration 3 | attr_accessor :mailer_sender, :cookie_expiration, :password_strategy, :user_model 4 | 5 | def initialize 6 | @mailer_sender = 'reply@example.com' 7 | @cookie_expiration = lambda { 1.year.from_now.utc } 8 | end 9 | 10 | def user_model 11 | @user_model || ::User 12 | end 13 | end 14 | 15 | class << self 16 | attr_accessor :configuration 17 | end 18 | 19 | # Configure Clearance someplace sensible, 20 | # like config/initializers/clearance.rb 21 | # 22 | # If you want users to only be signed in during the current session 23 | # instead of being remembered, do this: 24 | # 25 | # config.cookie_expiration = lambda { } 26 | # 27 | # @example 28 | # Clearance.configure do |config| 29 | # config.mailer_sender = 'me@example.com' 30 | # config.cookie_expiration = lambda { 2.weeks.from_now.utc } 31 | # config.password_strategy = MyPasswordStrategy 32 | # config.user_model = MyNamespace::MyUser 33 | # end 34 | def self.configure 35 | self.configuration ||= Configuration.new 36 | yield(configuration) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies/blowfish.rb: -------------------------------------------------------------------------------- 1 | require 'openssl' 2 | 3 | module Clearance 4 | module PasswordStrategies 5 | module Blowfish 6 | # Am I authenticated with given password? 7 | # 8 | # @param [String] plain-text password 9 | # @return [true, false] 10 | # @example 11 | # user.authenticated?('password') 12 | def authenticated?(password) 13 | encrypted_password == encrypt(password) 14 | end 15 | 16 | protected 17 | 18 | def encrypt_password 19 | initialize_salt_if_necessary 20 | if password.present? 21 | self.encrypted_password = encrypt(password) 22 | end 23 | end 24 | 25 | def generate_hash(string) 26 | cipher = OpenSSL::Cipher::Cipher.new('bf-cbc').encrypt 27 | cipher.key = Digest::SHA256.digest(salt) 28 | cipher.update(string) << cipher.final 29 | end 30 | 31 | def encrypt(string) 32 | generate_hash("--#{salt}--#{string}--") 33 | end 34 | 35 | def initialize_salt_if_necessary 36 | if salt.blank? 37 | self.salt = generate_random_code 38 | end 39 | end 40 | end 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/clearance/password_strategies/sha1.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | 3 | module Clearance 4 | module PasswordStrategies 5 | module SHA1 6 | # Am I authenticated with given password? 7 | # 8 | # @param [String] plain-text password 9 | # @return [true, false] 10 | # @example 11 | # user.authenticated?('password') 12 | def authenticated?(password) 13 | encrypted_password == encrypt(password) 14 | end 15 | 16 | protected 17 | 18 | def encrypt_password 19 | initialize_salt_if_necessary 20 | if password.present? 21 | self.encrypted_password = encrypt(password) 22 | end 23 | end 24 | 25 | def generate_hash(string) 26 | if RUBY_VERSION >= '1.9' 27 | Digest::SHA1.hexdigest(string).encode('UTF-8') 28 | else 29 | Digest::SHA1.hexdigest(string) 30 | end 31 | end 32 | 33 | def encrypt(string) 34 | generate_hash("--#{salt}--#{string}--") 35 | end 36 | 37 | def initialize_salt_if_necessary 38 | if salt.blank? 39 | self.salt = generate_random_code 40 | end 41 | end 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /spec/models/sha1_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::PasswordStrategies::SHA1 do 4 | subject do 5 | Class.new do 6 | attr_accessor :salt, :password, :encrypted_password 7 | include Clearance::PasswordStrategies::SHA1 8 | 9 | def generate_random_code; "code"; end 10 | end.new 11 | end 12 | 13 | describe "#encrypt_password" do 14 | context "when the password is set" do 15 | let(:salt) { "salt" } 16 | let(:password) { "password" } 17 | 18 | before do 19 | subject.salt = salt 20 | subject.password = password 21 | subject.send(:encrypt_password) 22 | end 23 | 24 | it "should encrypt the password using SHA1 into encrypted_password" do 25 | expected = Digest::SHA1.hexdigest("--#{salt}--#{password}--") 26 | 27 | subject.encrypted_password.should == expected 28 | end 29 | end 30 | 31 | context "when the salt is not set" do 32 | before do 33 | subject.salt = nil 34 | 35 | subject.send(:encrypt_password) 36 | end 37 | 38 | it "should initialize the salt" do 39 | subject.salt.should_not be_nil 40 | end 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /app/controllers/clearance/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Clearance::UsersController < ApplicationController 2 | unloadable 3 | 4 | skip_before_filter :authorize, :only => [:new, :create] 5 | before_filter :redirect_to_root, :only => [:new, :create], :if => :signed_in? 6 | 7 | def new 8 | @user = user_from_params 9 | render :template => 'users/new' 10 | end 11 | 12 | def create 13 | @user = user_from_params 14 | if @user.save 15 | sign_in(@user) 16 | redirect_back_or(url_after_create) 17 | else 18 | flash_failure_after_create 19 | render :template => 'users/new' 20 | end 21 | end 22 | 23 | private 24 | 25 | def flash_failure_after_create 26 | flash.now[:notice] = translate(:bad_email_or_password, 27 | :scope => [:clearance, :controllers, :passwords], 28 | :default => "Must be a valid email address. Password can't be blank.") 29 | end 30 | 31 | def url_after_create 32 | '/' 33 | end 34 | 35 | def user_from_params 36 | user_params = params[:user] || Hash.new 37 | email, password = user_params.delete(:email), user_params.delete(:password) 38 | Clearance.configuration.user_model.new(user_params).tap do |user| 39 | user.email = email 40 | user.password = password 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/models/blowfish_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::PasswordStrategies::Blowfish do 4 | subject do 5 | Class.new do 6 | attr_accessor :salt, :password, :encrypted_password 7 | include Clearance::PasswordStrategies::Blowfish 8 | 9 | def generate_random_code; "code"; end 10 | end.new 11 | end 12 | 13 | describe "#encrypt_password" do 14 | context "when the password is set" do 15 | let(:salt) { "salt" } 16 | let(:password) { "password" } 17 | 18 | before do 19 | subject.salt = salt 20 | subject.password = password 21 | subject.send(:encrypt_password) 22 | end 23 | 24 | it "should encrypt the password using Blowfish into encrypted_password" do 25 | cipher = OpenSSL::Cipher::Cipher.new('bf-cbc').encrypt 26 | cipher.key = Digest::SHA256.digest(salt) 27 | expected = cipher.update("--#{salt}--#{password}--") << cipher.final 28 | 29 | subject.encrypted_password.should == expected 30 | end 31 | end 32 | 33 | context "when the salt is not set" do 34 | before do 35 | subject.salt = nil 36 | 37 | subject.send(:encrypt_password) 38 | end 39 | 40 | it "should initialize the salt" do 41 | subject.salt.should_not be_nil 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/install_generator.rb: -------------------------------------------------------------------------------- 1 | require 'diesel/generators/install_base' 2 | 3 | module Clearance 4 | module Generators 5 | class InstallGenerator < Diesel::Generators::InstallBase 6 | def install 7 | template "clearance.rb", "config/initializers/clearance.rb" 8 | 9 | inject_into_class "app/controllers/application_controller.rb", ApplicationController do 10 | " include Clearance::Authentication\n" 11 | end 12 | 13 | user_model = "app/models/user.rb" 14 | if File.exists?(user_model) 15 | inject_into_class user_model, User do 16 | "include Clearance::User" 17 | end 18 | else 19 | template "user.rb", user_model 20 | end 21 | 22 | if File.exists?("spec") 23 | template "spec/factories.rb", "spec/factories/clearance.rb" 24 | else 25 | template "spec/factories.rb", "test/factories/clearance.rb" 26 | end 27 | 28 | readme "README" 29 | end 30 | 31 | private 32 | 33 | def migrations 34 | if users_table_exists? 35 | super.reject { |name| name.include?("create") } + ["db/migrate/upgrade_clearance_to_diesel.rb"] 36 | else 37 | super 38 | end 39 | end 40 | 41 | def users_table_exists? 42 | ActiveRecord::Base.connection.table_exists?(:users) 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/controllers/denies_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class DeniesController < ActionController::Base 4 | include Clearance::Authentication 5 | before_filter :authorize, :only => :show 6 | 7 | def new 8 | render :text => "New page" 9 | end 10 | 11 | def show 12 | render :text => "Show page" 13 | end 14 | 15 | protected 16 | 17 | def authorize 18 | deny_access("Access denied.") 19 | end 20 | end 21 | 22 | describe DeniesController do 23 | before do 24 | Rails.application.routes.draw do 25 | resource :deny, :only => [:new, :show] 26 | match 'sign_in' => 'clearance/sessions#new', :as => 'sign_in' 27 | end 28 | end 29 | 30 | after do 31 | Rails.application.reload_routes! 32 | end 33 | 34 | context "signed in user" do 35 | before { sign_in } 36 | 37 | it "allows access to new" do 38 | get :new 39 | subject.should_not deny_access 40 | end 41 | 42 | it "denies access to show" do 43 | get :show 44 | subject.should deny_access(:redirect => '/') 45 | end 46 | end 47 | 48 | context "visitor" do 49 | it "allows access to new" do 50 | get :new 51 | subject.should_not deny_access 52 | end 53 | 54 | it "denies access to show" do 55 | get :show 56 | subject.should deny_access 57 | subject.should deny_access(:redirect => sign_in_url, :flash => "Access denied.") 58 | end 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /spec/controllers/forgeries_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class ForgeriesController < ActionController::Base 4 | include Clearance::Authentication 5 | protect_from_forgery 6 | before_filter :authorize 7 | 8 | # This is off in test by default, but we need it for this test 9 | self.allow_forgery_protection = true 10 | 11 | def create 12 | redirect_to :action => 'index' 13 | end 14 | end 15 | 16 | describe ForgeriesController do 17 | context "signed in user" do 18 | before do 19 | Rails.application.routes.draw do 20 | resources :forgeries 21 | match 'sign_in' => 'clearance/sessions#new', :as => 'sign_in' 22 | end 23 | 24 | @user = create(:user) 25 | @user.update_attribute(:remember_token, "old-token") 26 | @request.cookies["remember_token"] = "old-token" 27 | @request.session[:_csrf_token] = "golden-ticket" 28 | end 29 | 30 | after do 31 | Rails.application.reload_routes! 32 | end 33 | 34 | it "succeeds with authentic token" do 35 | post :create, :authenticity_token => "golden-ticket" 36 | subject.should redirect_to(:action => 'index') 37 | end 38 | 39 | it "fails with invalid token" do 40 | post :create, :authenticity_token => "hax0r" 41 | subject.should deny_access 42 | end 43 | 44 | it "fails with no token" do 45 | post :create 46 | subject.should deny_access 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20110111224543) do 15 | 16 | create_table "users", :force => true do |t| 17 | t.string "email" 18 | t.string "encrypted_password", :limit => 128 19 | t.string "salt", :limit => 128 20 | t.string "confirmation_token", :limit => 128 21 | t.string "remember_token", :limit => 128 22 | t.datetime "created_at", :null => false 23 | t.datetime "updated_at", :null => false 24 | end 25 | 26 | add_index "users", ["email"], :name => "index_users_on_email" 27 | add_index "users", ["remember_token"], :name => "index_users_on_remember_token" 28 | 29 | end 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We love pull requests. Here's a quick guide: 2 | 3 | 1. Fork the repo. 4 | 5 | 2. Run the tests. We only take pull requests with passing tests, and it's great 6 | to know that you have a clean slate: `bundle && rake db:migrate && rake` 7 | 8 | 3. Add a test for your change. Only refactoring and documentation changes 9 | require no new tests. If you are adding functionality or fixing a bug, we need 10 | a test! 11 | 12 | 4. Make the test pass. 13 | 14 | 5. Push to your fork and submit a pull request. 15 | 16 | 17 | At this point you're waiting on us. We like to at least comment on, if not 18 | accept, pull requests within three business days (and, typically, one business 19 | day). We may suggest some changes or improvements or alternatives. 20 | 21 | Some things that will increase the chance that your pull request is accepted, 22 | taken straight from the Ruby on Rails guide: 23 | 24 | * Use Rails idioms and helpers 25 | * Include tests that fail without your code, and pass with it 26 | * Update the documentation, the surrounding one, examples elsewhere, guides, 27 | whatever is affected by your contribution 28 | 29 | Syntax: 30 | 31 | * Two spaces, no tabs. 32 | * No trailing whitespace. Blank lines should not have any space. 33 | * Prefer &&/|| over and/or. 34 | * MyClass.my_method(my_arg) not my_method( my_arg ) or my_method my_arg. 35 | * a = b and not a=b. 36 | * Follow the conventions you see used in the source already. 37 | 38 | And in case we didn't emphasize it enough: we love tests! 39 | -------------------------------------------------------------------------------- /lib/clearance/session.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | class Session 3 | REMEMBER_TOKEN_COOKIE = "remember_token".freeze 4 | 5 | def initialize(env) 6 | @env = env 7 | end 8 | 9 | def signed_in? 10 | current_user.present? 11 | end 12 | 13 | def current_user 14 | @current_user ||= with_remember_token do |token| 15 | Clearance.configuration.user_model.find_by_remember_token(token) 16 | end 17 | end 18 | 19 | def sign_in(user) 20 | @current_user = user 21 | end 22 | 23 | def sign_out 24 | current_user.reset_remember_token! if signed_in? 25 | @current_user = nil 26 | cookies.delete(REMEMBER_TOKEN_COOKIE) 27 | end 28 | 29 | def add_cookie_to_headers(headers) 30 | if signed_in? 31 | Rack::Utils.set_cookie_header!(headers, 32 | REMEMBER_TOKEN_COOKIE, 33 | :value => current_user.remember_token, 34 | :expires => Clearance.configuration.cookie_expiration.call, 35 | :path => "/") 36 | end 37 | end 38 | 39 | private 40 | 41 | def with_remember_token 42 | if token = remember_token 43 | yield token 44 | end 45 | end 46 | 47 | def remember_token 48 | cookies[REMEMBER_TOKEN_COOKIE] 49 | end 50 | 51 | def cookies 52 | @cookies ||= @env['action_dispatch.cookies'] || Rack::Request.new(@env).cookies 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /features/step_definitions/configuration_steps.rb: -------------------------------------------------------------------------------- 1 | When /^I configure ActionMailer to use "([^"]+)" as a host$/ do |host| 2 | mailer_config = "config.action_mailer.default_url_options = { :host => '#{host}' }" 3 | path = "config/application.rb" 4 | in_current_dir do 5 | contents = IO.read(path) 6 | contents.sub!(/(class .* < Rails::Application)/, "\\1\n#{mailer_config}") 7 | File.open(path, "w") { |file| file.write(contents) } 8 | end 9 | end 10 | 11 | When /^I configure a root route$/ do 12 | route = "root :to => 'home#show'" 13 | path = "config/routes.rb" 14 | in_current_dir do 15 | contents = IO.read(path) 16 | contents.sub!(/(\.routes\.draw do)/, "\\1\n#{route}\n") 17 | File.open(path, "w") { |file| file.write(contents) } 18 | end 19 | write_file("app/controllers/home_controller.rb", <<-CONTROLLER) 20 | class HomeController < ApplicationController 21 | def show 22 | render :text => "", :layout => "application" 23 | end 24 | end 25 | CONTROLLER 26 | end 27 | 28 | When /^I disable Capybara Javascript emulation$/ do 29 | in_current_dir do 30 | path = "features/support/env.rb" 31 | contents = IO.read(path) 32 | contents.sub!(%{require 'cucumber/rails/capybara_javascript_emulation'}, 33 | "# Disabled") 34 | File.open(path, "w") { |file| file.write(contents) } 35 | end 36 | end 37 | 38 | When /^I copy the locked Gemfile from this project$/ do 39 | in_current_dir do 40 | FileUtils.cp(File.join(PROJECT_ROOT, 'Gemfile.lock'), 'Gemfile.lock') 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/generators/clearance/install/templates/db/migrate/upgrade_clearance_to_diesel.rb: -------------------------------------------------------------------------------- 1 | class UpgradeClearanceToDiesel < ActiveRecord::Migration 2 | def self.up 3 | <% 4 | existing_columns = ActiveRecord::Base.connection.columns(:users).collect { |each| each.name } 5 | columns = [ 6 | [:email, 't.string :email'], 7 | [:encrypted_password, 't.string :encrypted_password, :limit => 128'], 8 | [:salt, 't.string :salt, :limit => 128'], 9 | [:confirmation_token, 't.string :confirmation_token, :limit => 128'], 10 | [:remember_token, 't.string :remember_token, :limit => 128'] 11 | ].delete_if {|c| existing_columns.include?(c.first.to_s)} 12 | -%> 13 | change_table(:users) do |t| 14 | <% columns.each do |c| -%> 15 | <%= c.last %> 16 | <% end -%> 17 | end 18 | 19 | <% 20 | existing_indexes = ActiveRecord::Base.connection.indexes(:users) 21 | index_names = existing_indexes.collect { |each| each.name } 22 | new_indexes = [ 23 | [:index_users_on_email, 'add_index :users, :email'], 24 | [:index_users_on_remember_token, 'add_index :users, :remember_token'] 25 | ].delete_if { |each| index_names.include?(each.first.to_s) } 26 | -%> 27 | <% new_indexes.each do |each| -%> 28 | <%= each.last %> 29 | <% end -%> 30 | end 31 | 32 | def self.down 33 | change_table(:users) do |t| 34 | <% unless columns.empty? -%> 35 | t.remove <%= columns.collect { |each| ":#{each.first}" }.join(',') %> 36 | <% end -%> 37 | end 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /features/integration_with_test_unit.feature: -------------------------------------------------------------------------------- 1 | Feature: integrate with test unit 2 | 3 | Background: 4 | When I successfully run `bundle exec rails new testapp` 5 | And I cd to "testapp" 6 | And I remove the file "public/index.html" 7 | And I remove the file "app/views/layouts/application.html.erb" 8 | And I configure ActionMailer to use "localhost" as a host 9 | And I configure a root route 10 | And I add the "factory_girl_rails" gem 11 | And I run `bundle install --local` 12 | 13 | Scenario: generate a Rails app, run the generators, and run the tests 14 | When I successfully run `bundle exec rails generate clearance:install` 15 | And I successfully run `bundle exec rake db:migrate --trace` 16 | And I successfully run `bundle exec rails generate controller posts index` 17 | And I add the "cucumber-rails" gem 18 | And I write to "test/test_helper.rb" with: 19 | """ 20 | ENV["RAILS_ENV"] = "test" 21 | require File.expand_path('../../config/environment', __FILE__) 22 | require 'rails/test_help' 23 | 24 | class ActiveSupport::TestCase 25 | fixtures :all 26 | end 27 | 28 | require 'clearance/testing' 29 | """ 30 | And I write to "test/functionals/posts_controller_test.rb" with: 31 | """ 32 | require 'test_helper' 33 | 34 | class PostsControllerTest < ActionController::TestCase 35 | test "should get index" do 36 | sign_in 37 | get :index 38 | assert_response :success 39 | end 40 | end 41 | """ 42 | And I successfully run `bundle exec rake --trace` 43 | Then the output should contain "1 tests, 1 assertions, 0 failures" 44 | -------------------------------------------------------------------------------- /clearance.gemspec: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.push File.expand_path('../lib', __FILE__) 2 | require 'clearance/version' 3 | require 'date' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = %q{clearance} 7 | s.version = Clearance::VERSION 8 | s.email = %q{support@thoughtbot.com} 9 | s.homepage = %q{http://github.com/thoughtbot/clearance} 10 | 11 | s.authors = ["Dan Croak", "Mike Burns", "Jason Morrison", "Joe Ferris", "Eugene Bolshakov", "Nick Quaranto", "Josh Nichols", "Mike Breen", "Jon Yurek", "Chad Pytel"] 12 | s.date = Date.today.to_s 13 | s.summary = %q{Rails authentication & authorization with email & password.} 14 | s.description = %q{Rails authentication & authorization with email & password.} 15 | s.extra_rdoc_files = %w(LICENSE README.md) 16 | s.files = `git ls-files`.split("\n") 17 | s.test_files = `git ls-files -- {test,features}/*`.split("\n") 18 | 19 | s.rdoc_options = ["--charset=UTF-8"] 20 | s.require_paths = ["lib"] 21 | 22 | s.add_dependency('rails', '>= 3.0') 23 | s.add_dependency('diesel', '~> 0.1.5') 24 | 25 | s.add_development_dependency('bundler', '~> 1.1.0') 26 | s.add_development_dependency('appraisal', '~> 0.4.1') 27 | s.add_development_dependency('cucumber-rails', '~> 1.1.1') 28 | s.add_development_dependency('rspec-rails', '~> 2.9.0') 29 | s.add_development_dependency('sqlite3') 30 | s.add_development_dependency('bourne', '~> 1.1.2') 31 | s.add_development_dependency('timecop') 32 | s.add_development_dependency('capybara', '~> 1.1.2') 33 | s.add_development_dependency('factory_girl_rails', '~> 3.1.0') 34 | s.add_development_dependency('shoulda-matchers', '~> 1.1.0') 35 | s.add_development_dependency('database_cleaner') 36 | s.add_development_dependency('launchy') 37 | s.add_development_dependency('aruba', '~> 0.4.11') 38 | end 39 | -------------------------------------------------------------------------------- /features/engine/visitor_resets_password.feature: -------------------------------------------------------------------------------- 1 | Feature: Password reset 2 | 3 | In order to sign in even if I forgot my password 4 | As a user 5 | I want to reset my password 6 | 7 | Scenario: User is not signed up 8 | When I reset the password for "unknown.email@example.com" 9 | Then I am told email is unknown 10 | 11 | Scenario: User is signed up and requests password reset 12 | Given I signed up with "email@example.com" 13 | When I reset the password for "email@example.com" 14 | Then instructions for changing my password are emailed to "email@example.com" 15 | 16 | Scenario: User tries to reset his password with a blank password 17 | Given I signed up with "email@example.com" 18 | When I reset the password for "email@example.com" 19 | And I follow the password reset link sent to "email@example.com" 20 | And I update my password with "" 21 | Then I am told to enter a password 22 | And I should be signed out 23 | 24 | Scenario: User is signed up and updates his password 25 | Given I signed up with "email@example.com" 26 | When I reset the password for "email@example.com" 27 | And I follow the password reset link sent to "email@example.com" 28 | And I update my password with "newpassword" 29 | Then I should be signed in 30 | When I sign out 31 | Then I should be signed out 32 | When I sign in with "email@example.com" and "newpassword" 33 | Then I should be signed in 34 | 35 | Scenario: User who was created before Clearance was installed creates password for first time 36 | Given a user "email@example.com" exists without a salt, remember token, or password 37 | When I reset the password for "email@example.com" 38 | When I follow the password reset link sent to "email@example.com" 39 | And I update my password with "newpassword" 40 | Then I should be signed in 41 | 42 | -------------------------------------------------------------------------------- /spec/support/cookies.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :set_cookie do |name, expected_value, expected_expires_at| 2 | match do |subject| 3 | @headers = subject 4 | @expected_name = name 5 | @expected_value = expected_value 6 | @expected_expires_at = expected_expires_at 7 | 8 | extract_cookies 9 | find_expected_cookie 10 | parse_expiration 11 | parse_value 12 | parse_path 13 | 14 | ensure_cookie_set 15 | ensure_expiration_correct 16 | ensure_path_is_correct 17 | end 18 | 19 | def extract_cookies 20 | @cookie_headers = @headers['Set-Cookie'] || [] 21 | @cookie_headers = [@cookie_headers] if @cookie_headers.respond_to?(:to_str) 22 | end 23 | 24 | def find_expected_cookie 25 | @cookie = @cookie_headers.detect do |header| 26 | header =~ /^#{@expected_name}=[^;]*(;|$)/ 27 | end 28 | end 29 | 30 | def parse_expiration 31 | if @cookie && result = @cookie.match(/; expires=(.*?)(;|$)/) 32 | @expires_at = Time.parse(result[1]) 33 | end 34 | end 35 | 36 | def parse_value 37 | if @cookie && result = @cookie.match(/=(.*?)(?:;|$)/) 38 | @value = result[1] 39 | end 40 | end 41 | 42 | def parse_path 43 | if @cookie && result = @cookie.match(/; path=(.*?)(;|$)/) 44 | @path = result[1] 45 | end 46 | end 47 | 48 | def ensure_cookie_set 49 | @value.should == @expected_value 50 | end 51 | 52 | def ensure_expiration_correct 53 | @expires_at.should_not be_nil 54 | @expires_at.should be_within(100).of(@expected_expires_at) 55 | end 56 | 57 | def ensure_path_is_correct 58 | @path.should == "/" 59 | end 60 | 61 | failure_message_for_should do 62 | "Expected #{expectation} got #{result}" 63 | end 64 | 65 | def expectation 66 | "a cookie named #{@expected_name} with value #{@expected_value.inspect} expiring at #{@expected_expires_at.inspect}" 67 | end 68 | 69 | def result 70 | if @cookie 71 | @cookie 72 | else 73 | @cookie_headers.join("; ") 74 | end 75 | end 76 | end 77 | -------------------------------------------------------------------------------- /features/integration.feature: -------------------------------------------------------------------------------- 1 | Feature: integrate with application 2 | 3 | Background: 4 | When I successfully run `bundle exec rails new testapp` 5 | And I cd to "testapp" 6 | And I remove the file "public/index.html" 7 | And I remove the file "app/views/layouts/application.html.erb" 8 | And I configure ActionMailer to use "localhost" as a host 9 | And I configure a root route 10 | And I add the "cucumber-rails" gem 11 | And I add the "capybara" gem 12 | And I add the "rspec-rails" gem 13 | And I add the "factory_girl_rails" gem 14 | And I add the "database_cleaner" gem 15 | And I add the "clearance" gem from this project 16 | And I run `bundle install --local` 17 | And I successfully run `bundle exec rails generate cucumber:install` 18 | And I disable Capybara Javascript emulation 19 | And I successfully run `bundle exec rails generate clearance:features` 20 | 21 | Scenario: generate a Rails app, run the generators, and run the tests 22 | When I successfully run `bundle exec rails generate clearance:install` 23 | Then the output should contain "Next steps" 24 | When I successfully run `bundle exec rake db:migrate --trace` 25 | And I successfully run `bundle exec rake --trace` 26 | Then the output should contain "passed" 27 | And the output should not contain "failed" 28 | And the output should not contain "Could not find generator" 29 | 30 | Scenario: Developer already has a users table in their database 31 | When I write to "db/migrate/001_create_users.rb" with: 32 | """ 33 | class CreateUsers < ActiveRecord::Migration 34 | def self.up 35 | create_table(:users) do |t| 36 | t.string :email 37 | t.string :name 38 | end 39 | end 40 | def self.down 41 | end 42 | end 43 | """ 44 | And I successfully run `bundle exec rake db:migrate --trace` 45 | And I successfully run `bundle exec rails generate clearance:install` 46 | And I successfully run `bundle exec rake db:migrate --trace` 47 | And I successfully run `bundle exec rake --trace` 48 | Then the output should contain "passed" 49 | And the output should not contain "failed" 50 | And the output should not contain "Could not find generator" 51 | 52 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::UsersController do 4 | describe "when signed out" do 5 | before { sign_out } 6 | 7 | describe "on GET to #new" do 8 | before { get :new } 9 | 10 | it { should respond_with(:success) } 11 | it { should render_template(:new) } 12 | it { should_not set_the_flash } 13 | end 14 | 15 | describe "on GET to #new with email" do 16 | before do 17 | @email = "a@example.com" 18 | get :new, :user => { :email => @email } 19 | end 20 | 21 | it "should set assigned user's email" do 22 | assigns(:user).email.should == @email 23 | end 24 | end 25 | 26 | describe "on POST to #create with valid attributes" do 27 | before do 28 | user_attributes = FactoryGirl.attributes_for(:user) 29 | @old_user_count = User.count 30 | post :create, :user => user_attributes 31 | end 32 | 33 | it { should assign_to(:user) } 34 | 35 | it "should create a new user" do 36 | User.count.should == @old_user_count + 1 37 | end 38 | 39 | it { should redirect_to_url_after_create } 40 | end 41 | 42 | describe "on POST to #create with valid attributes and a session return url" do 43 | before do 44 | user_attributes = FactoryGirl.attributes_for(:user) 45 | @old_user_count = User.count 46 | @return_url = '/url_in_the_session' 47 | @request.session[:return_to] = @return_url 48 | post :create, :user => user_attributes 49 | end 50 | 51 | it { should assign_to(:user) } 52 | 53 | it "should create a new user" do 54 | User.count.should == @old_user_count + 1 55 | end 56 | 57 | it { should redirect_to(@return_url) } 58 | end 59 | end 60 | 61 | describe "A signed-in user" do 62 | before do 63 | @user = create(:user) 64 | sign_in_as @user 65 | end 66 | 67 | describe "GET to new" do 68 | before { get :new } 69 | it "redirects to the home page" do 70 | should redirect_to(root_url) 71 | end 72 | end 73 | 74 | describe "POST to create" do 75 | before { post :create, :user => {} } 76 | it "redirects to the home page" do 77 | should redirect_to(root_url) 78 | end 79 | end 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /features/support/env.rb: -------------------------------------------------------------------------------- 1 | # IMPORTANT: This file is generated by cucumber-rails - edit at your own peril. 2 | # It is recommended to regenerate this file in the future when you upgrade to a 3 | # newer version of cucumber-rails. Consider adding your own code to a new file 4 | # instead of editing this one. Cucumber will automatically load all features/**/*.rb 5 | # files. 6 | 7 | ENV["RAILS_ENV"] ||= "test" 8 | 9 | PROJECT_ROOT = File.expand_path("../../..", __FILE__) 10 | $LOAD_PATH << File.join(PROJECT_ROOT, "lib") 11 | 12 | require 'rails/all' 13 | 14 | Bundler.require 15 | 16 | require 'diesel/testing' 17 | require 'diesel/testing/integration' 18 | require 'cucumber/rails/application' 19 | require 'cucumber/rails/action_controller' 20 | require 'rails/test_help' 21 | require 'cucumber/rails/world' 22 | require 'cucumber/rails/hooks' 23 | require 'cucumber/rails/capybara' 24 | 25 | # Capybara defaults to XPath selectors rather than Webrat's default of CSS3. In 26 | # order to ease the transition to Capybara we set the default here. If you'd 27 | # prefer to use XPath just remove this line and adjust any selectors in your 28 | # steps to use the XPath syntax. 29 | Capybara.default_selector = :css 30 | Capybara.save_and_open_page_path = 'tmp' 31 | 32 | # By default, any exception happening in your Rails application will bubble up 33 | # to Cucumber so that your scenario will fail. This is a different from how 34 | # your application behaves in the production environment, where an error page will 35 | # be rendered instead. 36 | # 37 | # Sometimes we want to override this default behaviour and allow Rails to rescue 38 | # exceptions and display an error page (just like when the app is running in production). 39 | # Typical scenarios where you want to do this is when you test your error pages. 40 | # There are two ways to allow Rails to rescue exceptions: 41 | # 42 | # 1) Tag your scenario (or feature) with @allow-rescue 43 | # 44 | # 2) Set the value below to true. Beware that doing this globally is not 45 | # recommended as it will mask a lot of errors for you! 46 | # 47 | ActionController::Base.allow_rescue = false 48 | 49 | # Remove/comment out the lines below if your app doesn't have a database. 50 | # For some databases (like MongoDB and CouchDB) you may need to use :truncation instead. 51 | begin 52 | DatabaseCleaner.strategy = :transaction 53 | rescue NameError 54 | raise "You need to add database_cleaner to your Gemfile (in the :test group) if you wish to use it." 55 | end 56 | -------------------------------------------------------------------------------- /lib/clearance/testing/deny_access_matcher.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Testing 3 | module Matchers 4 | # Ensures a controller denied access. 5 | # 6 | # @example 7 | # it { should deny_access } 8 | # it { should deny_access(:flash => "Denied access.") } 9 | # it { should deny_access(:redirect => sign_in_url) } 10 | def deny_access(opts = {}) 11 | DenyAccessMatcher.new(self, opts) 12 | end 13 | 14 | class DenyAccessMatcher 15 | attr_reader :failure_message, :negative_failure_message 16 | 17 | def initialize(context, opts) 18 | @context = context 19 | @flash = opts[:flash] 20 | @url = opts[:redirect] 21 | 22 | @failure_message = "" 23 | @negative_failure_message = "" 24 | end 25 | 26 | def matches?(controller) 27 | @controller = controller 28 | sets_the_flash? && redirects_to_url? 29 | end 30 | 31 | def description 32 | "deny access" 33 | end 34 | 35 | private 36 | 37 | def sets_the_flash? 38 | if @flash.blank? 39 | true 40 | else 41 | if flash_notice_value == @flash 42 | @negative_failure_message << "Didn't expect to set the flash to #{@flash}" 43 | true 44 | else 45 | @failure_message << "Expected the flash to be set to #{@flash} but was #{flash_notice_value}" 46 | false 47 | end 48 | end 49 | end 50 | 51 | def flash_notice_value 52 | if flash_notice.respond_to?(:values) 53 | flash_notice.values.first 54 | else 55 | flash_notice 56 | end 57 | end 58 | 59 | def flash_notice 60 | @controller.flash[:notice] 61 | end 62 | 63 | def redirects_to_url? 64 | @url ||= denied_access_url 65 | begin 66 | @context.send(:assert_redirected_to, @url) 67 | @negative_failure_message << "Didn't expect to redirect to #{@url}." 68 | true 69 | rescue Clearance::Testing::AssertionError 70 | @failure_message << "Expected to redirect to #{@url} but did not." 71 | false 72 | end 73 | end 74 | 75 | def denied_access_url 76 | if @controller.signed_in? 77 | '/' 78 | else 79 | @controller.sign_in_url 80 | end 81 | end 82 | end 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /app/controllers/clearance/passwords_controller.rb: -------------------------------------------------------------------------------- 1 | class Clearance::PasswordsController < ApplicationController 2 | unloadable 3 | 4 | skip_before_filter :authorize, :only => [:new, :create, :edit, :update] 5 | before_filter :forbid_missing_token, :only => [:edit, :update] 6 | before_filter :forbid_non_existent_user, :only => [:edit, :update] 7 | 8 | def new 9 | render :template => 'passwords/new' 10 | end 11 | 12 | def create 13 | if user = Clearance.configuration.user_model.find_by_email( 14 | params[:password][:email]) 15 | user.forgot_password! 16 | ::ClearanceMailer.change_password(user).deliver 17 | render :template => 'passwords/create' 18 | else 19 | flash_failure_after_create 20 | render :template => 'passwords/new' 21 | end 22 | end 23 | 24 | def edit 25 | @user = Clearance.configuration.user_model.find_by_id_and_confirmation_token( 26 | params[:user_id], params[:token].to_s) 27 | render :template => 'passwords/edit' 28 | end 29 | 30 | def update 31 | @user = Clearance.configuration.user_model.find_by_id_and_confirmation_token( 32 | params[:user_id], params[:token].to_s) 33 | 34 | if @user.update_password(params[:user][:password]) 35 | sign_in(@user) 36 | redirect_to(url_after_update) 37 | else 38 | flash_failure_after_update 39 | render :template => 'passwords/edit' 40 | end 41 | end 42 | 43 | private 44 | 45 | def forbid_missing_token 46 | if params[:token].to_s.blank? 47 | flash_failure_when_forbidden 48 | render :template => 'passwords/new' 49 | end 50 | end 51 | 52 | def forbid_non_existent_user 53 | unless Clearance.configuration.user_model.find_by_id_and_confirmation_token( 54 | params[:user_id], params[:token].to_s) 55 | flash_failure_when_forbidden 56 | render :template => 'passwords/new' 57 | end 58 | end 59 | 60 | def flash_failure_when_forbidden 61 | flash.now[:notice] = translate(:forbidden, 62 | :scope => [:clearance, :controllers, :passwords], 63 | :default => "Please double check the URL or try submitting the form again.") 64 | end 65 | 66 | def flash_failure_after_create 67 | flash.now[:notice] = translate(:unknown_email, 68 | :scope => [:clearance, :controllers, :passwords], 69 | :default => "Unknown email.") 70 | end 71 | 72 | def flash_failure_after_update 73 | flash.now[:notice] = translate(:blank_password, 74 | :scope => [:clearance, :controllers, :passwords], 75 | :default => "Password can't be blank.") 76 | end 77 | 78 | def url_after_create 79 | sign_in_url 80 | end 81 | 82 | def url_after_update 83 | '/' 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::SessionsController do 4 | describe "on GET to /sessions/new" do 5 | before { get :new } 6 | 7 | it { should respond_with(:success) } 8 | it { should render_template(:new) } 9 | it { should_not set_the_flash } 10 | end 11 | 12 | describe "on POST to #create with good credentials" do 13 | before do 14 | @user = create(:user) 15 | @user.update_attribute(:remember_token, "old-token") 16 | post :create, :session => { 17 | :email => @user.email, 18 | :password => @user.password } 19 | end 20 | 21 | it { should redirect_to_url_after_create } 22 | 23 | it "sets the user in the clearance session" do 24 | controller.current_user.should == @user 25 | end 26 | 27 | it "should not change the remember token" do 28 | @user.reload.remember_token.should == "old-token" 29 | end 30 | end 31 | 32 | describe "on POST to #create with good credentials and a session return url" do 33 | before do 34 | @user = create(:user) 35 | @return_url = '/url_in_the_session' 36 | @request.session[:return_to] = @return_url 37 | post :create, :session => { 38 | :email => @user.email, 39 | :password => @user.password } 40 | end 41 | 42 | it "redirects to the return URL" do 43 | should redirect_to(@return_url) 44 | end 45 | end 46 | 47 | describe "on POST to #create with good credentials and a request return url" do 48 | before do 49 | @user = create(:user) 50 | @return_url = '/url_in_the_request' 51 | post :create, :session => { 52 | :email => @user.email, 53 | :password => @user.password }, 54 | :return_to => @return_url 55 | end 56 | 57 | it "redirects to the return URL" do 58 | should redirect_to(@return_url) 59 | end 60 | end 61 | 62 | describe "on POST to #create with good credentials and a session return url and request return url" do 63 | before do 64 | @user = create(:user) 65 | @return_url = '/url_in_the_session' 66 | @request.session[:return_to] = @return_url 67 | post :create, :session => { 68 | :email => @user.email, 69 | :password => @user.password }, 70 | :return_to => '/url_in_the_request' 71 | end 72 | 73 | it "redirects to the return url" do 74 | should redirect_to(@return_url) 75 | end 76 | end 77 | 78 | describe "on DELETE to #destroy given a signed out user" do 79 | before do 80 | sign_out 81 | delete :destroy 82 | end 83 | it { should redirect_to_url_after_destroy } 84 | end 85 | 86 | describe "on DELETE to #destroy with a cookie" do 87 | before do 88 | @user = create(:user) 89 | @user.update_attribute(:remember_token, "old-token") 90 | @request.cookies["remember_token"] = "old-token" 91 | delete :destroy 92 | end 93 | 94 | it { should redirect_to_url_after_destroy } 95 | 96 | it "should reset the remember token" do 97 | @user.reload.remember_token.should_not == "old-token" 98 | end 99 | 100 | it "should unset the current user" do 101 | @controller.current_user.should be_nil 102 | end 103 | end 104 | end 105 | -------------------------------------------------------------------------------- /spec/clearance/session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::Session do 4 | before { Timecop.freeze } 5 | after { Timecop.return } 6 | 7 | it "finds a user from a cookie" do 8 | user = create(:user) 9 | env = env_with_remember_token(user.remember_token) 10 | 11 | session = Clearance::Session.new(env) 12 | session.should be_signed_in 13 | session.current_user.should == user 14 | end 15 | 16 | it "returns nil for an unknown user" do 17 | user = create(:user) 18 | env = env_with_remember_token("bogus") 19 | 20 | session = Clearance::Session.new(env) 21 | session.should_not be_signed_in 22 | session.current_user.should be_nil 23 | end 24 | 25 | it "returns nil without a remember token" do 26 | env = env_without_remember_token 27 | session = Clearance::Session.new(env) 28 | session.should_not be_signed_in 29 | session.current_user.should be_nil 30 | end 31 | 32 | it "signs in a given user" do 33 | user = create(:user) 34 | session = Clearance::Session.new(env_without_remember_token) 35 | session.sign_in user 36 | session.current_user.should == user 37 | end 38 | 39 | it "sets a remember token cookie with a default expiration of 1 year from now" do 40 | user = create(:user) 41 | headers = {} 42 | session = Clearance::Session.new(env_without_remember_token) 43 | session.sign_in user 44 | session.add_cookie_to_headers headers 45 | headers.should set_cookie("remember_token", user.remember_token, 1.year.from_now) 46 | end 47 | 48 | it "sets a remember token cookie with a custom expiration" do 49 | custom_expiration = 1.day.from_now 50 | with_custom_expiration 1.day.from_now do 51 | user = create(:user) 52 | headers = {} 53 | session = Clearance::Session.new(env_without_remember_token) 54 | session.sign_in user 55 | session.add_cookie_to_headers headers 56 | headers.should set_cookie("remember_token", user.remember_token, 1.day.from_now) 57 | Clearance.configuration.cookie_expiration.call.should be_within(100).of(1.year.from_now) 58 | end 59 | end 60 | 61 | it "doesn't set a remember token when signed out" do 62 | headers = {} 63 | session = Clearance::Session.new(env_without_remember_token) 64 | session.add_cookie_to_headers headers 65 | headers.should_not set_cookie("remember_token") 66 | end 67 | 68 | it "signs out a user" do 69 | user = create(:user) 70 | old_remember_token = user.remember_token 71 | env = env_with_remember_token(old_remember_token) 72 | 73 | session = Clearance::Session.new(env) 74 | session.sign_out 75 | session.current_user.should be_nil 76 | user.reload.remember_token.should_not == old_remember_token 77 | end 78 | 79 | def env_with_remember_token(token) 80 | env_with_cookies("remember_token" => token) 81 | end 82 | 83 | def env_without_remember_token 84 | env_with_cookies({}) 85 | end 86 | 87 | def env_with_cookies(cookies) 88 | Rack::MockRequest.env_for("/", "HTTP_COOKIE" => serialize_cookies(cookies)) 89 | end 90 | 91 | def serialize_cookies(hash) 92 | header = {} 93 | hash.each do |key, value| 94 | Rack::Utils.set_cookie_header!(header, key, value) 95 | end 96 | header['Set-Cookie'] 97 | end 98 | 99 | def with_custom_expiration(custom_duration) 100 | Clearance.configuration.cookie_expiration = lambda { custom_duration } 101 | ensure 102 | restore_default_config 103 | end 104 | 105 | def restore_default_config 106 | Clearance.configuration = nil 107 | Clearance.configure {} 108 | end 109 | end 110 | -------------------------------------------------------------------------------- /features/step_definitions/engine/clearance_steps.rb: -------------------------------------------------------------------------------- 1 | # Existing users 2 | 3 | require 'factory_girl_rails' 4 | 5 | Given /^(?:I am|I have|I) signed up (?:as|with) "(.*)"$/ do |email| 6 | FactoryGirl.create(:user, :email => email) 7 | end 8 | 9 | Given /^a user "([^"]*)" exists without a salt, remember token, or password$/ do |email| 10 | user = FactoryGirl.create(:user, :email => email) 11 | sql = "update users set salt = NULL, encrypted_password = NULL, remember_token = NULL where id = #{user.id}" 12 | ActiveRecord::Base.connection.update(sql) 13 | end 14 | 15 | # Sign up 16 | 17 | When /^I sign up (?:with|as) "(.*)" and "(.*)"$/ do |email, password| 18 | visit sign_up_path 19 | page.should have_css("input[type='email']") 20 | 21 | fill_in "Email", :with => email 22 | fill_in "Password", :with => password 23 | click_button "Sign up" 24 | end 25 | 26 | # Sign in 27 | 28 | Given /^I sign in$/ do 29 | email = FactoryGirl.generate(:email) 30 | steps %{ 31 | I have signed up with "#{email}" 32 | I sign in with "#{email}" 33 | } 34 | end 35 | 36 | When /^I sign in (?:with|as) "([^"]*)"$/ do |email| 37 | step %{I sign in with "#{email}" and "password"} 38 | end 39 | 40 | When /^I sign in (?:with|as) "([^"]*)" and "([^"]*)"$/ do |email, password| 41 | visit sign_in_path 42 | page.should have_css("input[type='email']") 43 | 44 | fill_in "Email", :with => email 45 | fill_in "Password", :with => password 46 | click_button "Sign in" 47 | end 48 | 49 | # Sign out 50 | 51 | When "I sign out" do 52 | visit "/" 53 | click_link "Sign out" 54 | end 55 | 56 | # Reset password 57 | 58 | When /^I reset the password for "(.*)"$/ do |email| 59 | visit new_password_path 60 | page.should have_css("input[type='email']") 61 | 62 | fill_in "Email address", :with => email 63 | click_button "Reset password" 64 | end 65 | 66 | Then /^instructions for changing my password are emailed to "(.*)"$/ do |email| 67 | page.should have_content("instructions for changing your password") 68 | 69 | user = User.find_by_email!(email) 70 | assert !user.confirmation_token.blank? 71 | assert !ActionMailer::Base.deliveries.empty? 72 | result = ActionMailer::Base.deliveries.any? do |email| 73 | email.to == [user.email] && 74 | email.subject =~ /password/i && 75 | email.body =~ /#{user.confirmation_token}/ 76 | end 77 | assert result 78 | end 79 | 80 | When /^I follow the password reset link sent to "(.*)"$/ do |email| 81 | user = User.find_by_email!(email) 82 | visit edit_user_password_path(:user_id => user, 83 | :token => user.confirmation_token) 84 | end 85 | 86 | When /^I change the password of "(.*)" without token$/ do |email| 87 | user = User.find_by_email!(email) 88 | visit edit_user_password_path(:user_id => user) 89 | end 90 | 91 | When /^I update my password with "(.*)"$/ do |password| 92 | fill_in "Choose password", :with => password 93 | click_button "Save this password" 94 | end 95 | 96 | # Flashes 97 | 98 | Then /^I am told email or password is bad$/ do 99 | page.should have_content("Bad email or password") 100 | end 101 | 102 | Then /^I am told email is unknown$/ do 103 | page.should have_content("Unknown email") 104 | end 105 | 106 | Then /^I am told to enter a valid email address$/ do 107 | page.should have_content("Must be a valid email address") 108 | end 109 | 110 | Then /^I am told to enter a password$/ do 111 | page.should have_content("Password can't be blank") 112 | end 113 | 114 | # Verification 115 | 116 | Then /^I should be signed in$/ do 117 | visit "/" 118 | page.should have_content "Sign out" 119 | end 120 | 121 | Then /^I should be signed out$/ do 122 | visit "/" 123 | page.should have_content "Sign in" 124 | end 125 | -------------------------------------------------------------------------------- /lib/clearance/authentication.rb: -------------------------------------------------------------------------------- 1 | module Clearance 2 | module Authentication 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | helper_method :current_user, :signed_in?, :signed_out? 7 | hide_action :current_user, :current_user=, 8 | :signed_in?, :signed_out?, 9 | :sign_in, :sign_out, 10 | :authorize, :deny_access 11 | end 12 | 13 | # Finds the user from the rack clearance session 14 | # 15 | # @return [User, nil] 16 | def current_user 17 | clearance_session.current_user 18 | end 19 | 20 | # Set the current user 21 | # 22 | # @param [User] 23 | def current_user=(user) 24 | clearance_session.sign_in user 25 | end 26 | 27 | # Is the current user signed in? 28 | # 29 | # @return [true, false] 30 | def signed_in? 31 | clearance_session.signed_in? 32 | end 33 | 34 | # Is the current user signed out? 35 | # 36 | # @return [true, false] 37 | def signed_out? 38 | !signed_in? 39 | end 40 | 41 | # Sign user in to cookie. 42 | # 43 | # @param [User] 44 | # 45 | # @example 46 | # sign_in(@user) 47 | def sign_in(user) 48 | clearance_session.sign_in user 49 | end 50 | 51 | # Sign user out of cookie. 52 | # 53 | # @example 54 | # sign_out 55 | def sign_out 56 | clearance_session.sign_out 57 | end 58 | 59 | # Find the user by the given params or return nil. 60 | # By default, uses email and password. 61 | # Redefine this method and User.authenticate for other mechanisms 62 | # such as username and password. 63 | # 64 | # @example 65 | # @user = authenticate(params) 66 | def authenticate(params) 67 | Clearance.configuration.user_model.authenticate(params[:session][:email], 68 | params[:session][:password]) 69 | end 70 | 71 | # Deny the user access if they are signed out. 72 | # 73 | # @example 74 | # before_filter :authorize 75 | def authorize 76 | deny_access unless signed_in? 77 | end 78 | 79 | # Store the current location and redirect to sign in. 80 | # Display a failure flash message if included. 81 | # 82 | # @param [String] optional flash message to display to denied user 83 | def deny_access(flash_message = nil) 84 | store_location 85 | flash[:notice] = flash_message if flash_message 86 | if signed_in? 87 | redirect_to(url_after_denied_access_when_signed_in) 88 | else 89 | redirect_to(url_after_denied_access_when_signed_out) 90 | end 91 | end 92 | 93 | # CSRF protection in Rails >= 3.0.4 94 | # http://weblog.rubyonrails.org/2011/2/8/csrf-protection-bypass-in-ruby-on-rails 95 | def handle_unverified_request 96 | super 97 | sign_out 98 | end 99 | 100 | protected 101 | 102 | def clearance_session 103 | request.env[:clearance] 104 | end 105 | 106 | def store_location 107 | if request.get? 108 | session[:return_to] = request.fullpath 109 | end 110 | end 111 | 112 | def redirect_back_or(default) 113 | redirect_to(return_to || default) 114 | clear_return_to 115 | end 116 | 117 | def return_to 118 | session[:return_to] || params[:return_to] 119 | end 120 | 121 | def clear_return_to 122 | session[:return_to] = nil 123 | end 124 | 125 | def redirect_to_root 126 | redirect_to('/') 127 | end 128 | 129 | def url_after_denied_access_when_signed_in 130 | '/' 131 | end 132 | 133 | def url_after_denied_access_when_signed_out 134 | sign_in_url 135 | end 136 | end 137 | end 138 | -------------------------------------------------------------------------------- /gemfiles/3.0.12.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /home/mike/clearance 3 | specs: 4 | clearance (0.16.2) 5 | diesel (~> 0.1.5) 6 | rails (>= 3.0) 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | abstract (1.0.0) 12 | actionmailer (3.0.12) 13 | actionpack (= 3.0.12) 14 | mail (~> 2.2.19) 15 | actionpack (3.0.12) 16 | activemodel (= 3.0.12) 17 | activesupport (= 3.0.12) 18 | builder (~> 2.1.2) 19 | erubis (~> 2.6.6) 20 | i18n (~> 0.5.0) 21 | rack (~> 1.2.5) 22 | rack-mount (~> 0.6.14) 23 | rack-test (~> 0.5.7) 24 | tzinfo (~> 0.3.23) 25 | activemodel (3.0.12) 26 | activesupport (= 3.0.12) 27 | builder (~> 2.1.2) 28 | i18n (~> 0.5.0) 29 | activerecord (3.0.12) 30 | activemodel (= 3.0.12) 31 | activesupport (= 3.0.12) 32 | arel (~> 2.0.10) 33 | tzinfo (~> 0.3.23) 34 | activeresource (3.0.12) 35 | activemodel (= 3.0.12) 36 | activesupport (= 3.0.12) 37 | activesupport (3.0.12) 38 | addressable (2.2.7) 39 | appraisal (0.4.1) 40 | bundler 41 | rake 42 | arel (2.0.10) 43 | aruba (0.4.11) 44 | childprocess (>= 0.2.3) 45 | cucumber (>= 1.1.1) 46 | ffi (>= 1.0.11) 47 | rspec (>= 2.7.0) 48 | bourne (1.1.2) 49 | mocha (= 0.10.5) 50 | builder (2.1.2) 51 | capybara (1.1.2) 52 | mime-types (>= 1.16) 53 | nokogiri (>= 1.3.3) 54 | rack (>= 1.0.0) 55 | rack-test (>= 0.5.4) 56 | selenium-webdriver (~> 2.0) 57 | xpath (~> 0.1.4) 58 | childprocess (0.3.1) 59 | ffi (~> 1.0.6) 60 | cucumber (1.1.9) 61 | builder (>= 2.1.2) 62 | diff-lcs (>= 1.1.2) 63 | gherkin (~> 2.9.0) 64 | json (>= 1.4.6) 65 | term-ansicolor (>= 1.0.6) 66 | cucumber-rails (1.1.1) 67 | capybara (>= 1.1.1) 68 | cucumber (>= 1.1.0) 69 | nokogiri (>= 1.5.0) 70 | database_cleaner (0.7.2) 71 | diesel (0.1.5) 72 | railties 73 | diff-lcs (1.1.3) 74 | erubis (2.6.6) 75 | abstract (>= 1.0.0) 76 | factory_girl (3.1.1) 77 | activesupport (>= 3.0.0) 78 | factory_girl_rails (3.1.0) 79 | factory_girl (~> 3.1.0) 80 | railties (>= 3.0.0) 81 | ffi (1.0.11) 82 | gherkin (2.9.3) 83 | json (>= 1.4.6) 84 | i18n (0.5.0) 85 | json (1.6.6) 86 | launchy (2.1.0) 87 | addressable (~> 2.2.6) 88 | mail (2.2.19) 89 | activesupport (>= 2.3.6) 90 | i18n (>= 0.4.0) 91 | mime-types (~> 1.16) 92 | treetop (~> 1.4.8) 93 | metaclass (0.0.1) 94 | mime-types (1.18) 95 | mocha (0.10.5) 96 | metaclass (~> 0.0.1) 97 | multi_json (1.2.0) 98 | nokogiri (1.5.2) 99 | polyglot (0.3.3) 100 | rack (1.2.5) 101 | rack-mount (0.6.14) 102 | rack (>= 1.0.0) 103 | rack-test (0.5.7) 104 | rack (>= 1.0) 105 | rails (3.0.12) 106 | actionmailer (= 3.0.12) 107 | actionpack (= 3.0.12) 108 | activerecord (= 3.0.12) 109 | activeresource (= 3.0.12) 110 | activesupport (= 3.0.12) 111 | bundler (~> 1.0) 112 | railties (= 3.0.12) 113 | railties (3.0.12) 114 | actionpack (= 3.0.12) 115 | activesupport (= 3.0.12) 116 | rake (>= 0.8.7) 117 | rdoc (~> 3.4) 118 | thor (~> 0.14.4) 119 | rake (0.9.2.2) 120 | rdoc (3.12) 121 | json (~> 1.4) 122 | rspec (2.9.0) 123 | rspec-core (~> 2.9.0) 124 | rspec-expectations (~> 2.9.0) 125 | rspec-mocks (~> 2.9.0) 126 | rspec-core (2.9.0) 127 | rspec-expectations (2.9.1) 128 | diff-lcs (~> 1.1.3) 129 | rspec-mocks (2.9.0) 130 | rspec-rails (2.9.0) 131 | actionpack (>= 3.0) 132 | activesupport (>= 3.0) 133 | railties (>= 3.0) 134 | rspec (~> 2.9.0) 135 | rubyzip (0.9.6.1) 136 | selenium-webdriver (2.20.0) 137 | childprocess (>= 0.2.5) 138 | ffi (~> 1.0) 139 | multi_json (~> 1.0) 140 | rubyzip 141 | shoulda-matchers (1.1.0) 142 | activesupport (>= 3.0.0) 143 | sqlite3 (1.3.5) 144 | term-ansicolor (1.0.7) 145 | thor (0.14.6) 146 | timecop (0.3.5) 147 | treetop (1.4.10) 148 | polyglot 149 | polyglot (>= 0.3.1) 150 | tzinfo (0.3.32) 151 | xpath (0.1.4) 152 | nokogiri (~> 1.3) 153 | 154 | PLATFORMS 155 | ruby 156 | 157 | DEPENDENCIES 158 | appraisal (~> 0.4.1) 159 | aruba (~> 0.4.11) 160 | bourne (~> 1.1.2) 161 | bundler (~> 1.1.0) 162 | capybara (~> 1.1.2) 163 | clearance! 164 | cucumber-rails (~> 1.1.1) 165 | database_cleaner 166 | factory_girl_rails (~> 3.1.0) 167 | launchy 168 | rails (= 3.0.12) 169 | rspec-rails (~> 2.9.0) 170 | shoulda-matchers (~> 1.1.0) 171 | sqlite3 172 | timecop 173 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | clearance (0.16.2) 5 | diesel (~> 0.1.5) 6 | rails (>= 3.0) 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | actionmailer (3.2.1) 12 | actionpack (= 3.2.1) 13 | mail (~> 2.4.0) 14 | actionpack (3.2.1) 15 | activemodel (= 3.2.1) 16 | activesupport (= 3.2.1) 17 | builder (~> 3.0.0) 18 | erubis (~> 2.7.0) 19 | journey (~> 1.0.1) 20 | rack (~> 1.4.0) 21 | rack-cache (~> 1.1) 22 | rack-test (~> 0.6.1) 23 | sprockets (~> 2.1.2) 24 | activemodel (3.2.1) 25 | activesupport (= 3.2.1) 26 | builder (~> 3.0.0) 27 | activerecord (3.2.1) 28 | activemodel (= 3.2.1) 29 | activesupport (= 3.2.1) 30 | arel (~> 3.0.0) 31 | tzinfo (~> 0.3.29) 32 | activeresource (3.2.1) 33 | activemodel (= 3.2.1) 34 | activesupport (= 3.2.1) 35 | activesupport (3.2.1) 36 | i18n (~> 0.6) 37 | multi_json (~> 1.0) 38 | addressable (2.2.6) 39 | appraisal (0.4.1) 40 | bundler 41 | rake 42 | arel (3.0.2) 43 | aruba (0.4.11) 44 | childprocess (>= 0.2.3) 45 | cucumber (>= 1.1.1) 46 | ffi (>= 1.0.11) 47 | rspec (>= 2.7.0) 48 | bourne (1.1.2) 49 | mocha (= 0.10.5) 50 | builder (3.0.0) 51 | capybara (1.1.2) 52 | mime-types (>= 1.16) 53 | nokogiri (>= 1.3.3) 54 | rack (>= 1.0.0) 55 | rack-test (>= 0.5.4) 56 | selenium-webdriver (~> 2.0) 57 | xpath (~> 0.1.4) 58 | childprocess (0.3.0) 59 | ffi (~> 1.0.6) 60 | cucumber (1.1.4) 61 | builder (>= 2.1.2) 62 | diff-lcs (>= 1.1.2) 63 | gherkin (~> 2.7.1) 64 | json (>= 1.4.6) 65 | term-ansicolor (>= 1.0.6) 66 | cucumber-rails (1.1.1) 67 | capybara (>= 1.1.1) 68 | cucumber (>= 1.1.0) 69 | nokogiri (>= 1.5.0) 70 | database_cleaner (0.7.1) 71 | diesel (0.1.5) 72 | railties 73 | diff-lcs (1.1.3) 74 | erubis (2.7.0) 75 | factory_girl (3.1.1) 76 | activesupport (>= 3.0.0) 77 | factory_girl_rails (3.1.0) 78 | factory_girl (~> 3.1.0) 79 | railties (>= 3.0.0) 80 | ffi (1.0.11) 81 | gherkin (2.7.6) 82 | json (>= 1.4.6) 83 | hike (1.2.1) 84 | i18n (0.6.0) 85 | journey (1.0.3) 86 | json (1.6.6) 87 | launchy (2.0.5) 88 | addressable (~> 2.2.6) 89 | mail (2.4.4) 90 | i18n (>= 0.4.0) 91 | mime-types (~> 1.16) 92 | treetop (~> 1.4.8) 93 | metaclass (0.0.1) 94 | mime-types (1.17.2) 95 | mocha (0.10.5) 96 | metaclass (~> 0.0.1) 97 | multi_json (1.0.4) 98 | nokogiri (1.5.0) 99 | polyglot (0.3.3) 100 | rack (1.4.1) 101 | rack-cache (1.2) 102 | rack (>= 0.4) 103 | rack-ssl (1.3.2) 104 | rack 105 | rack-test (0.6.1) 106 | rack (>= 1.0) 107 | rails (3.2.1) 108 | actionmailer (= 3.2.1) 109 | actionpack (= 3.2.1) 110 | activerecord (= 3.2.1) 111 | activeresource (= 3.2.1) 112 | activesupport (= 3.2.1) 113 | bundler (~> 1.0) 114 | railties (= 3.2.1) 115 | railties (3.2.1) 116 | actionpack (= 3.2.1) 117 | activesupport (= 3.2.1) 118 | rack-ssl (~> 1.3.2) 119 | rake (>= 0.8.7) 120 | rdoc (~> 3.4) 121 | thor (~> 0.14.6) 122 | rake (0.9.2.2) 123 | rdoc (3.12) 124 | json (~> 1.4) 125 | rspec (2.9.0) 126 | rspec-core (~> 2.9.0) 127 | rspec-expectations (~> 2.9.0) 128 | rspec-mocks (~> 2.9.0) 129 | rspec-core (2.9.0) 130 | rspec-expectations (2.9.1) 131 | diff-lcs (~> 1.1.3) 132 | rspec-mocks (2.9.0) 133 | rspec-rails (2.9.0) 134 | actionpack (>= 3.0) 135 | activesupport (>= 3.0) 136 | railties (>= 3.0) 137 | rspec (~> 2.9.0) 138 | rubyzip (0.9.5) 139 | selenium-webdriver (2.18.0) 140 | childprocess (>= 0.2.5) 141 | ffi (~> 1.0.9) 142 | multi_json (~> 1.0.4) 143 | rubyzip 144 | shoulda-matchers (1.1.0) 145 | activesupport (>= 3.0.0) 146 | sprockets (2.1.2) 147 | hike (~> 1.2) 148 | rack (~> 1.0) 149 | tilt (~> 1.1, != 1.3.0) 150 | sqlite3 (1.3.5) 151 | term-ansicolor (1.0.7) 152 | thor (0.14.6) 153 | tilt (1.3.3) 154 | timecop (0.3.5) 155 | treetop (1.4.10) 156 | polyglot 157 | polyglot (>= 0.3.1) 158 | tzinfo (0.3.33) 159 | xpath (0.1.4) 160 | nokogiri (~> 1.3) 161 | 162 | PLATFORMS 163 | ruby 164 | 165 | DEPENDENCIES 166 | appraisal (~> 0.4.1) 167 | aruba (~> 0.4.11) 168 | bourne (~> 1.1.2) 169 | bundler (~> 1.1.0) 170 | capybara (~> 1.1.2) 171 | clearance! 172 | cucumber-rails (~> 1.1.1) 173 | database_cleaner 174 | factory_girl_rails (~> 3.1.0) 175 | launchy 176 | rspec-rails (~> 2.9.0) 177 | shoulda-matchers (~> 1.1.0) 178 | sqlite3 179 | timecop 180 | -------------------------------------------------------------------------------- /gemfiles/3.2.3.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /home/mike/clearance 3 | specs: 4 | clearance (0.16.2) 5 | diesel (~> 0.1.5) 6 | rails (>= 3.0) 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | actionmailer (3.2.3) 12 | actionpack (= 3.2.3) 13 | mail (~> 2.4.4) 14 | actionpack (3.2.3) 15 | activemodel (= 3.2.3) 16 | activesupport (= 3.2.3) 17 | builder (~> 3.0.0) 18 | erubis (~> 2.7.0) 19 | journey (~> 1.0.1) 20 | rack (~> 1.4.0) 21 | rack-cache (~> 1.2) 22 | rack-test (~> 0.6.1) 23 | sprockets (~> 2.1.2) 24 | activemodel (3.2.3) 25 | activesupport (= 3.2.3) 26 | builder (~> 3.0.0) 27 | activerecord (3.2.3) 28 | activemodel (= 3.2.3) 29 | activesupport (= 3.2.3) 30 | arel (~> 3.0.2) 31 | tzinfo (~> 0.3.29) 32 | activeresource (3.2.3) 33 | activemodel (= 3.2.3) 34 | activesupport (= 3.2.3) 35 | activesupport (3.2.3) 36 | i18n (~> 0.6) 37 | multi_json (~> 1.0) 38 | addressable (2.2.7) 39 | appraisal (0.4.1) 40 | bundler 41 | rake 42 | arel (3.0.2) 43 | aruba (0.4.11) 44 | childprocess (>= 0.2.3) 45 | cucumber (>= 1.1.1) 46 | ffi (>= 1.0.11) 47 | rspec (>= 2.7.0) 48 | bourne (1.1.2) 49 | mocha (= 0.10.5) 50 | builder (3.0.0) 51 | capybara (1.1.2) 52 | mime-types (>= 1.16) 53 | nokogiri (>= 1.3.3) 54 | rack (>= 1.0.0) 55 | rack-test (>= 0.5.4) 56 | selenium-webdriver (~> 2.0) 57 | xpath (~> 0.1.4) 58 | childprocess (0.3.1) 59 | ffi (~> 1.0.6) 60 | cucumber (1.1.9) 61 | builder (>= 2.1.2) 62 | diff-lcs (>= 1.1.2) 63 | gherkin (~> 2.9.0) 64 | json (>= 1.4.6) 65 | term-ansicolor (>= 1.0.6) 66 | cucumber-rails (1.1.1) 67 | capybara (>= 1.1.1) 68 | cucumber (>= 1.1.0) 69 | nokogiri (>= 1.5.0) 70 | database_cleaner (0.7.2) 71 | diesel (0.1.5) 72 | railties 73 | diff-lcs (1.1.3) 74 | erubis (2.7.0) 75 | factory_girl (3.1.1) 76 | activesupport (>= 3.0.0) 77 | factory_girl_rails (3.1.0) 78 | factory_girl (~> 3.1.0) 79 | railties (>= 3.0.0) 80 | ffi (1.0.11) 81 | gherkin (2.9.3) 82 | json (>= 1.4.6) 83 | hike (1.2.1) 84 | i18n (0.6.0) 85 | journey (1.0.3) 86 | json (1.6.6) 87 | launchy (2.1.0) 88 | addressable (~> 2.2.6) 89 | mail (2.4.4) 90 | i18n (>= 0.4.0) 91 | mime-types (~> 1.16) 92 | treetop (~> 1.4.8) 93 | metaclass (0.0.1) 94 | mime-types (1.18) 95 | mocha (0.10.5) 96 | metaclass (~> 0.0.1) 97 | multi_json (1.2.0) 98 | nokogiri (1.5.2) 99 | polyglot (0.3.3) 100 | rack (1.4.1) 101 | rack-cache (1.2) 102 | rack (>= 0.4) 103 | rack-ssl (1.3.2) 104 | rack 105 | rack-test (0.6.1) 106 | rack (>= 1.0) 107 | rails (3.2.3) 108 | actionmailer (= 3.2.3) 109 | actionpack (= 3.2.3) 110 | activerecord (= 3.2.3) 111 | activeresource (= 3.2.3) 112 | activesupport (= 3.2.3) 113 | bundler (~> 1.0) 114 | railties (= 3.2.3) 115 | railties (3.2.3) 116 | actionpack (= 3.2.3) 117 | activesupport (= 3.2.3) 118 | rack-ssl (~> 1.3.2) 119 | rake (>= 0.8.7) 120 | rdoc (~> 3.4) 121 | thor (~> 0.14.6) 122 | rake (0.9.2.2) 123 | rdoc (3.12) 124 | json (~> 1.4) 125 | rspec (2.9.0) 126 | rspec-core (~> 2.9.0) 127 | rspec-expectations (~> 2.9.0) 128 | rspec-mocks (~> 2.9.0) 129 | rspec-core (2.9.0) 130 | rspec-expectations (2.9.1) 131 | diff-lcs (~> 1.1.3) 132 | rspec-mocks (2.9.0) 133 | rspec-rails (2.9.0) 134 | actionpack (>= 3.0) 135 | activesupport (>= 3.0) 136 | railties (>= 3.0) 137 | rspec (~> 2.9.0) 138 | rubyzip (0.9.6.1) 139 | selenium-webdriver (2.20.0) 140 | childprocess (>= 0.2.5) 141 | ffi (~> 1.0) 142 | multi_json (~> 1.0) 143 | rubyzip 144 | shoulda-matchers (1.1.0) 145 | activesupport (>= 3.0.0) 146 | sprockets (2.1.2) 147 | hike (~> 1.2) 148 | rack (~> 1.0) 149 | tilt (~> 1.1, != 1.3.0) 150 | sqlite3 (1.3.5) 151 | term-ansicolor (1.0.7) 152 | thor (0.14.6) 153 | tilt (1.3.3) 154 | timecop (0.3.5) 155 | treetop (1.4.10) 156 | polyglot 157 | polyglot (>= 0.3.1) 158 | tzinfo (0.3.32) 159 | xpath (0.1.4) 160 | nokogiri (~> 1.3) 161 | 162 | PLATFORMS 163 | ruby 164 | 165 | DEPENDENCIES 166 | appraisal (~> 0.4.1) 167 | aruba (~> 0.4.11) 168 | bourne (~> 1.1.2) 169 | bundler (~> 1.1.0) 170 | capybara (~> 1.1.2) 171 | clearance! 172 | cucumber-rails (~> 1.1.1) 173 | database_cleaner 174 | factory_girl_rails (~> 3.1.0) 175 | launchy 176 | rails (= 3.2.3) 177 | rspec-rails (~> 2.9.0) 178 | shoulda-matchers (~> 1.1.0) 179 | sqlite3 180 | timecop 181 | -------------------------------------------------------------------------------- /gemfiles/3.1.4.gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: /home/mike/clearance 3 | specs: 4 | clearance (0.16.2) 5 | diesel (~> 0.1.5) 6 | rails (>= 3.0) 7 | 8 | GEM 9 | remote: http://rubygems.org/ 10 | specs: 11 | actionmailer (3.1.4) 12 | actionpack (= 3.1.4) 13 | mail (~> 2.3.0) 14 | actionpack (3.1.4) 15 | activemodel (= 3.1.4) 16 | activesupport (= 3.1.4) 17 | builder (~> 3.0.0) 18 | erubis (~> 2.7.0) 19 | i18n (~> 0.6) 20 | rack (~> 1.3.6) 21 | rack-cache (~> 1.1) 22 | rack-mount (~> 0.8.2) 23 | rack-test (~> 0.6.1) 24 | sprockets (~> 2.0.3) 25 | activemodel (3.1.4) 26 | activesupport (= 3.1.4) 27 | builder (~> 3.0.0) 28 | i18n (~> 0.6) 29 | activerecord (3.1.4) 30 | activemodel (= 3.1.4) 31 | activesupport (= 3.1.4) 32 | arel (~> 2.2.3) 33 | tzinfo (~> 0.3.29) 34 | activeresource (3.1.4) 35 | activemodel (= 3.1.4) 36 | activesupport (= 3.1.4) 37 | activesupport (3.1.4) 38 | multi_json (~> 1.0) 39 | addressable (2.2.7) 40 | appraisal (0.4.1) 41 | bundler 42 | rake 43 | arel (2.2.3) 44 | aruba (0.4.11) 45 | childprocess (>= 0.2.3) 46 | cucumber (>= 1.1.1) 47 | ffi (>= 1.0.11) 48 | rspec (>= 2.7.0) 49 | bourne (1.1.2) 50 | mocha (= 0.10.5) 51 | builder (3.0.0) 52 | capybara (1.1.2) 53 | mime-types (>= 1.16) 54 | nokogiri (>= 1.3.3) 55 | rack (>= 1.0.0) 56 | rack-test (>= 0.5.4) 57 | selenium-webdriver (~> 2.0) 58 | xpath (~> 0.1.4) 59 | childprocess (0.3.1) 60 | ffi (~> 1.0.6) 61 | cucumber (1.1.9) 62 | builder (>= 2.1.2) 63 | diff-lcs (>= 1.1.2) 64 | gherkin (~> 2.9.0) 65 | json (>= 1.4.6) 66 | term-ansicolor (>= 1.0.6) 67 | cucumber-rails (1.1.1) 68 | capybara (>= 1.1.1) 69 | cucumber (>= 1.1.0) 70 | nokogiri (>= 1.5.0) 71 | database_cleaner (0.7.2) 72 | diesel (0.1.5) 73 | railties 74 | diff-lcs (1.1.3) 75 | erubis (2.7.0) 76 | factory_girl (3.1.1) 77 | activesupport (>= 3.0.0) 78 | factory_girl_rails (3.1.0) 79 | factory_girl (~> 3.1.0) 80 | railties (>= 3.0.0) 81 | ffi (1.0.11) 82 | gherkin (2.9.3) 83 | json (>= 1.4.6) 84 | hike (1.2.1) 85 | i18n (0.6.0) 86 | json (1.6.6) 87 | launchy (2.1.0) 88 | addressable (~> 2.2.6) 89 | mail (2.3.3) 90 | i18n (>= 0.4.0) 91 | mime-types (~> 1.16) 92 | treetop (~> 1.4.8) 93 | metaclass (0.0.1) 94 | mime-types (1.18) 95 | mocha (0.10.5) 96 | metaclass (~> 0.0.1) 97 | multi_json (1.2.0) 98 | nokogiri (1.5.2) 99 | polyglot (0.3.3) 100 | rack (1.3.6) 101 | rack-cache (1.2) 102 | rack (>= 0.4) 103 | rack-mount (0.8.3) 104 | rack (>= 1.0.0) 105 | rack-ssl (1.3.2) 106 | rack 107 | rack-test (0.6.1) 108 | rack (>= 1.0) 109 | rails (3.1.4) 110 | actionmailer (= 3.1.4) 111 | actionpack (= 3.1.4) 112 | activerecord (= 3.1.4) 113 | activeresource (= 3.1.4) 114 | activesupport (= 3.1.4) 115 | bundler (~> 1.0) 116 | railties (= 3.1.4) 117 | railties (3.1.4) 118 | actionpack (= 3.1.4) 119 | activesupport (= 3.1.4) 120 | rack-ssl (~> 1.3.2) 121 | rake (>= 0.8.7) 122 | rdoc (~> 3.4) 123 | thor (~> 0.14.6) 124 | rake (0.9.2.2) 125 | rdoc (3.12) 126 | json (~> 1.4) 127 | rspec (2.9.0) 128 | rspec-core (~> 2.9.0) 129 | rspec-expectations (~> 2.9.0) 130 | rspec-mocks (~> 2.9.0) 131 | rspec-core (2.9.0) 132 | rspec-expectations (2.9.1) 133 | diff-lcs (~> 1.1.3) 134 | rspec-mocks (2.9.0) 135 | rspec-rails (2.9.0) 136 | actionpack (>= 3.0) 137 | activesupport (>= 3.0) 138 | railties (>= 3.0) 139 | rspec (~> 2.9.0) 140 | rubyzip (0.9.6.1) 141 | selenium-webdriver (2.20.0) 142 | childprocess (>= 0.2.5) 143 | ffi (~> 1.0) 144 | multi_json (~> 1.0) 145 | rubyzip 146 | shoulda-matchers (1.1.0) 147 | activesupport (>= 3.0.0) 148 | sprockets (2.0.3) 149 | hike (~> 1.2) 150 | rack (~> 1.0) 151 | tilt (~> 1.1, != 1.3.0) 152 | sqlite3 (1.3.5) 153 | term-ansicolor (1.0.7) 154 | thor (0.14.6) 155 | tilt (1.3.3) 156 | timecop (0.3.5) 157 | treetop (1.4.10) 158 | polyglot 159 | polyglot (>= 0.3.1) 160 | tzinfo (0.3.32) 161 | xpath (0.1.4) 162 | nokogiri (~> 1.3) 163 | 164 | PLATFORMS 165 | ruby 166 | 167 | DEPENDENCIES 168 | appraisal (~> 0.4.1) 169 | aruba (~> 0.4.11) 170 | bourne (~> 1.1.2) 171 | bundler (~> 1.1.0) 172 | capybara (~> 1.1.2) 173 | clearance! 174 | cucumber-rails (~> 1.1.1) 175 | database_cleaner 176 | factory_girl_rails (~> 3.1.0) 177 | launchy 178 | rails (= 3.1.4) 179 | rspec-rails (~> 2.9.0) 180 | shoulda-matchers (~> 1.1.0) 181 | sqlite3 182 | timecop 183 | -------------------------------------------------------------------------------- /lib/clearance/user.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha1' 2 | 3 | module Clearance 4 | module User 5 | extend ActiveSupport::Concern 6 | 7 | # Hook for all Clearance::User modules. 8 | # 9 | # If you need to override parts of Clearance::User, 10 | # extend and include à la carte. 11 | # 12 | # @example 13 | # include Clearance::User::Callbacks 14 | # 15 | # @see Validations 16 | # @see Callbacks 17 | included do 18 | attr_accessor :password_changing 19 | attr_reader :password 20 | 21 | include Validations 22 | include Callbacks 23 | 24 | include (Clearance.configuration.password_strategy || Clearance::PasswordStrategies::SHA1) 25 | end 26 | 27 | module ClassMethods 28 | # Authenticate with email and password. 29 | # 30 | # @param [String, String] email and password 31 | # @return [User, nil] authenticated user or nil 32 | # @example 33 | # User.authenticate("email@example.com", "password") 34 | def authenticate(email, password) 35 | return nil unless user = find_by_email(email.to_s.downcase) 36 | return user if user.authenticated?(password) 37 | end 38 | end 39 | 40 | module Validations 41 | extend ActiveSupport::Concern 42 | 43 | # Hook for validations. 44 | # 45 | # :email must be present, unique, formatted 46 | # 47 | # If password is required, 48 | # :password must be present, confirmed 49 | included do 50 | validates_presence_of :email, :unless => :email_optional? 51 | validates_uniqueness_of :email, :allow_blank => true 52 | validates_format_of :email, :with => %r{^[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#\$%&'*+\/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$}i, :allow_blank => true 53 | 54 | validates_presence_of :password, :unless => :password_optional? 55 | end 56 | end 57 | 58 | module Callbacks 59 | extend ActiveSupport::Concern 60 | 61 | # Hook for callbacks. 62 | # 63 | # salt, token, password encryption are handled before_save. 64 | included do 65 | before_validation :downcase_email 66 | before_create :generate_remember_token 67 | end 68 | end 69 | 70 | # Set the remember token. 71 | # 72 | # @deprecated Use {#reset_remember_token!} instead 73 | def remember_me! 74 | warn "[DEPRECATION] remember_me!: use reset_remember_token! instead" 75 | reset_remember_token! 76 | end 77 | 78 | # Reset the remember token. 79 | # 80 | # @example 81 | # user.reset_remember_token! 82 | def reset_remember_token! 83 | generate_remember_token 84 | save(:validate => false) 85 | end 86 | 87 | # Mark my account as forgotten password. 88 | # 89 | # @example 90 | # user.forgot_password! 91 | def forgot_password! 92 | generate_confirmation_token 93 | save(:validate => false) 94 | end 95 | 96 | # Update my password. 97 | # 98 | # @return [true, false] password was updated or not 99 | # @example 100 | # user.update_password('new-password') 101 | def update_password(new_password) 102 | self.password_changing = true 103 | self.password = new_password 104 | if valid? 105 | self.confirmation_token = nil 106 | generate_remember_token 107 | end 108 | save 109 | end 110 | 111 | def password=(unencrypted_password) 112 | @password = unencrypted_password 113 | encrypt_password 114 | end 115 | 116 | protected 117 | 118 | def generate_random_code(length = 20) 119 | if RUBY_VERSION >= '1.9' 120 | SecureRandom.hex(length).encode('UTF-8') 121 | else 122 | SecureRandom.hex(length) 123 | end 124 | end 125 | 126 | def generate_remember_token 127 | self.remember_token = generate_random_code 128 | end 129 | 130 | def generate_confirmation_token 131 | self.confirmation_token = generate_random_code 132 | end 133 | 134 | # Always false. Override to allow other forms of authentication 135 | # (username, facebook, etc). 136 | # @return [Boolean] true if the email field be left blank for this user 137 | def email_optional? 138 | false 139 | end 140 | 141 | # True if the password has been set and the password is not being 142 | # updated and we are not updating the password. Override to allow 143 | # other forms of authentication (username, facebook, etc). 144 | # @return [Boolean] true if the password field can be left blank for this user 145 | def password_optional? 146 | encrypted_password.present? && password.blank? && password_changing.blank? 147 | end 148 | 149 | def password_required? 150 | # warn "[DEPRECATION] password_required?: use !password_optional? instead" 151 | !password_optional? 152 | end 153 | 154 | def downcase_email 155 | self.email = email.to_s.downcase 156 | end 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /spec/controllers/passwords_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Clearance::PasswordsController do 4 | include Shoulda::Matchers::ActionMailer 5 | 6 | it { should route(:get, '/users/1/password/edit'). 7 | to(:controller => 'clearance/passwords', :action => 'edit', :user_id => '1') } 8 | 9 | describe "a signed up user" do 10 | before do 11 | @user = create(:user) 12 | end 13 | 14 | describe "on GET to #new" do 15 | before { get :new, :user_id => @user.to_param } 16 | 17 | it { should respond_with(:success) } 18 | it { should render_template(:new) } 19 | end 20 | 21 | describe "on POST to #create" do 22 | describe "with correct email address" do 23 | before do 24 | ActionMailer::Base.deliveries.clear 25 | post :create, :password => { :email => @user.email } 26 | end 27 | 28 | it "should generate a token for the change your password email" do 29 | @user.reload.confirmation_token.should_not be_nil 30 | end 31 | 32 | it { should have_sent_email.with_subject(/change your password/i) } 33 | 34 | it { should respond_with(:success) } 35 | end 36 | 37 | describe "with incorrect email address" do 38 | before do 39 | email = "user1@example.com" 40 | (Clearance.configuration.user_model.exists?(['email = ?', email])).should_not be 41 | ActionMailer::Base.deliveries.clear 42 | @user.reload.confirmation_token.should == @user.confirmation_token 43 | 44 | post :create, :password => { :email => email } 45 | end 46 | 47 | it "should not generate a token for the change your password email" do 48 | @user.reload.confirmation_token.should == @user.confirmation_token 49 | end 50 | 51 | it "should not send a password reminder email" do 52 | ActionMailer::Base.deliveries.should be_empty 53 | end 54 | 55 | it { should set_the_flash.to(/unknown email/i).now } 56 | it { should render_template(:new) } 57 | end 58 | end 59 | end 60 | 61 | describe "a signed up user and forgotten password" do 62 | before do 63 | @user = create(:user) 64 | @user.forgot_password! 65 | end 66 | 67 | describe "on GET to #edit with correct id and token" do 68 | before do 69 | get :edit, :user_id => @user.to_param, 70 | :token => @user.confirmation_token 71 | end 72 | 73 | it "should find the user" do 74 | assigns(:user).should == @user 75 | end 76 | 77 | it { should respond_with(:success) } 78 | it { should render_template(:edit) } 79 | end 80 | 81 | describe "on GET to #edit with correct id but blank token" do 82 | before do 83 | get :edit, :user_id => @user.to_param, :token => "" 84 | end 85 | 86 | it { should set_the_flash.to(/double check the URL/i).now } 87 | it { should render_template(:new) } 88 | end 89 | 90 | describe "on GET to #edit with correct id but no token" do 91 | before do 92 | get :edit, :user_id => @user.to_param 93 | end 94 | 95 | it { should set_the_flash.to(/double check the URL/i).now } 96 | it { should render_template(:new) } 97 | end 98 | 99 | describe "on PUT to #update with password" do 100 | before do 101 | new_password = "new_password" 102 | @encrypted_new_password = @user.send(:encrypt, new_password) 103 | @user.encrypted_password.should_not == @encrypted_new_password 104 | 105 | put(:update, 106 | :user_id => @user, 107 | :token => @user.confirmation_token, 108 | :user => { 109 | :password => new_password 110 | }) 111 | @user.reload 112 | end 113 | 114 | it "should update password" do 115 | @user.encrypted_password.should == @encrypted_new_password 116 | end 117 | 118 | it "should clear confirmation token" do 119 | @user.confirmation_token.should be_nil 120 | end 121 | 122 | it "should set remember token" do 123 | @user.remember_token.should_not be_nil 124 | end 125 | 126 | it { should redirect_to_url_after_update } 127 | end 128 | 129 | describe "on PUT to #update with blank password" do 130 | before do 131 | put(:update, 132 | :user_id => @user.to_param, 133 | :token => @user.confirmation_token, 134 | :user => { 135 | :password => '' 136 | }) 137 | @user.reload 138 | end 139 | 140 | it "should not update password" do 141 | @user.encrypted_password.should_not == @encrypted_new_password 142 | end 143 | 144 | it "should not clear token" do 145 | @user.confirmation_token.should_not be_nil 146 | end 147 | 148 | it "should not be signed in" do 149 | cookies[:remember_token].should be_nil 150 | end 151 | 152 | it { should set_the_flash.to(/password can't be blank/i).now } 153 | it { should respond_with(:success) } 154 | it { should render_template(:edit) } 155 | end 156 | 157 | describe "on PUT to #update with an empty token after the user sets a password" do 158 | before do 159 | put :update, 160 | :user_id => @user.to_param, 161 | :token => @user.confirmation_token, 162 | :user => { :password => 'good password' } 163 | put :update, 164 | :user_id => @user.to_param, 165 | :token => [nil], 166 | :user => { :password => 'new password' } 167 | end 168 | 169 | it { should set_the_flash.to(/double check the URL/i).now } 170 | it { should render_template(:new) } 171 | end 172 | end 173 | 174 | describe "given two users and user one signs in" do 175 | before do 176 | @user_one = create(:user) 177 | @user_two = create(:user) 178 | sign_in_as @user_one 179 | end 180 | end 181 | end 182 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe User do 4 | it { should have_db_index(:email) } 5 | it { should have_db_index(:remember_token) } 6 | 7 | describe "When signing up" do 8 | it { should validate_presence_of(:email) } 9 | it { should validate_presence_of(:password) } 10 | it { should allow_value("foo@example.co.uk").for(:email) } 11 | it { should allow_value("foo@example.com").for(:email) } 12 | it { should_not allow_value("foo@").for(:email) } 13 | it { should_not allow_value("foo@example..com").for(:email) } 14 | it { should_not allow_value("foo@.example.com").for(:email) } 15 | it { should_not allow_value("foo").for(:email) } 16 | it { should_not allow_value("example.com").for(:email) } 17 | 18 | it "should store email in down case" do 19 | user = create(:user, :email => "John.Doe@example.com") 20 | user.email.should == "john.doe@example.com" 21 | end 22 | end 23 | 24 | describe "When multiple users have signed up" do 25 | before { create(:user) } 26 | it { should validate_uniqueness_of(:email) } 27 | end 28 | 29 | describe "A user" do 30 | before do 31 | @user = create(:user) 32 | @password = @user.password 33 | end 34 | 35 | it "is authenticated with correct email and password" do 36 | (Clearance.configuration.user_model.authenticate(@user.email, @password)).should be 37 | @user.should be_authenticated(@password) 38 | end 39 | 40 | it "is authenticated with correct uppercased email and correct password" do 41 | (Clearance.configuration.user_model.authenticate(@user.email.upcase, @password)).should be 42 | @user.should be_authenticated(@password) 43 | end 44 | 45 | it "is authenticated with incorrect credentials" do 46 | (Clearance.configuration.user_model.authenticate(@user.email, 'bad_password')).should_not be 47 | @user.should_not be_authenticated('bad password') 48 | end 49 | end 50 | 51 | describe "When resetting authentication with reset_remember_token!" do 52 | before do 53 | @user = create(:user) 54 | @user.remember_token = "old-token" 55 | @user.reset_remember_token! 56 | end 57 | 58 | it "should change the remember token" do 59 | @user.remember_token.should_not == "old-token" 60 | end 61 | end 62 | 63 | describe "An email confirmed user" do 64 | before do 65 | @user = create(:user) 66 | @old_encrypted_password = @user.encrypted_password 67 | end 68 | 69 | describe "who updates password" do 70 | before do 71 | @user.update_password("new_password") 72 | end 73 | 74 | it "should change encrypted password" do 75 | @user.encrypted_password.should_not == @old_encrypted_password 76 | end 77 | end 78 | end 79 | 80 | it "should not generate the same remember token for users with the same password at the same time" do 81 | Time.stubs(:now => Time.now) 82 | password = 'secret' 83 | first_user = create(:user, :password => password) 84 | second_user = create(:user, :password => password) 85 | 86 | second_user.remember_token.should_not == first_user.remember_token 87 | end 88 | 89 | describe "An user" do 90 | before do 91 | @user = create(:user) 92 | @old_encrypted_password = @user.encrypted_password 93 | end 94 | 95 | describe "who requests password reminder" do 96 | before do 97 | @user.confirmation_token.should be_nil 98 | @user.forgot_password! 99 | end 100 | 101 | it "should generate confirmation token" do 102 | @user.confirmation_token.should_not be_nil 103 | end 104 | 105 | describe "and then updates password" do 106 | describe 'with password' do 107 | before do 108 | @user.update_password("new_password") 109 | end 110 | 111 | it "should change encrypted password" do 112 | @user.encrypted_password.should_not == @old_encrypted_password 113 | end 114 | 115 | it "should clear confirmation token" do 116 | @user.confirmation_token.should be_nil 117 | end 118 | end 119 | 120 | describe 'with blank password' do 121 | before do 122 | @user.update_password("") 123 | end 124 | 125 | it "does not change encrypted password" do 126 | @user.encrypted_password.should == @old_encrypted_password 127 | end 128 | 129 | it "does not clear confirmation token" do 130 | @user.confirmation_token.should_not be_nil 131 | end 132 | end 133 | end 134 | end 135 | end 136 | 137 | describe "a user with an optional email" do 138 | before do 139 | @user = User.new 140 | class << @user 141 | def email_optional? 142 | true 143 | end 144 | end 145 | end 146 | 147 | subject { @user } 148 | 149 | it { should allow_value(nil).for(:email) } 150 | it { should allow_value("").for(:email) } 151 | end 152 | 153 | describe "a user with an optional password" do 154 | before do 155 | @user = User.new 156 | class << @user 157 | def password_optional? 158 | true 159 | end 160 | end 161 | end 162 | 163 | subject { @user } 164 | 165 | it { should allow_value(nil).for(:password) } 166 | it { should allow_value("").for(:password) } 167 | end 168 | 169 | describe "user factory" do 170 | it "should create a valid user with just an overridden password" do 171 | build(:user, :password => "test").should be_valid 172 | end 173 | end 174 | 175 | describe "when user exists before Clearance was installed" do 176 | before do 177 | @user = create(:user) 178 | sql = "update users set salt = NULL, encrypted_password = NULL, remember_token = NULL where id = #{@user.id}" 179 | ActiveRecord::Base.connection.update(sql) 180 | @user.reload.salt.should be_nil 181 | @user.reload.encrypted_password.should be_nil 182 | @user.reload.remember_token.should be_nil 183 | end 184 | 185 | it "should initialize salt, generate remember token, and save encrypted password on update_password" do 186 | @user.update_password('password') 187 | @user.salt.should_not be_nil 188 | @user.encrypted_password.should_not be_nil 189 | @user.remember_token.should_not be_nil 190 | end 191 | end 192 | 193 | describe "The password setter on a User" do 194 | let(:password) { "a-password" } 195 | before { subject.send(:password=, password) } 196 | 197 | it "sets password to the plain-text password" do 198 | subject.password.should == password 199 | end 200 | 201 | it "also sets encrypted_password" do 202 | subject.encrypted_password.should_not be_nil 203 | end 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Clearance [![Build Status](https://secure.travis-ci.org/thoughtbot/clearance.png)](http://travis-ci.org/thoughtbot/clearance?branch=master) 2 | ========= 3 | 4 | Rails authentication & authorization with email & password. 5 | 6 | [We have clearance, Clarence.](http://www.youtube.com/watch?v=fVq4_HhBK8Y) 7 | 8 | Clearance was extracted out of [Airbrake](http://airbrakeapp.com/). 9 | 10 | Help 11 | ---- 12 | 13 | * [Documentation](http://rdoc.info/gems/clearance) at RDoc.info. 14 | * [Patches and bugs](http://github.com/thoughtbot/clearance/issues) at Github Issues. 15 | * [Mailing list](http://groups.google.com/group/thoughtbot-clearance) at Google Groups. 16 | 17 | Installation 18 | ------------ 19 | 20 | Clearance is a Rails engine for Rails 3. It is currently tested against Rails 3.0.12 and Rails 3.1.4. 21 | 22 | Include the gem in your Gemfile: 23 | 24 | gem "clearance" 25 | 26 | Make sure the development database exists, then run the generator: 27 | 28 | rails generate clearance:install 29 | 30 | This: 31 | 32 | * inserts Clearance::User into your User model 33 | * inserts Clearance::Authentication into your ApplicationController 34 | * creates a migration that either creates a users table or adds only missing columns 35 | 36 | Follow the instructions that are output from the generator. 37 | 38 | Use the [0.8.x](https://github.com/thoughtbot/clearance/tree/v0.8.8) 39 | series of Clearance if you have a Rails 2 app. 40 | 41 | Usage 42 | ----- 43 | 44 | If you want to authorize users for a controller action, use the authorize 45 | method in a before_filter. 46 | 47 | class WidgetsController < ApplicationController 48 | before_filter :authorize 49 | 50 | def index 51 | @widgets = Widget.all 52 | end 53 | end 54 | 55 | If you want to reference the current user in a controller, view, or helper, use 56 | the current_user method. 57 | 58 | def index 59 | current_user.articles 60 | end 61 | 62 | If you want to know whether the current user is signed in or out, you can use 63 | these methods in controllers, views, or helpers: 64 | 65 | signed_in? 66 | signed_out? 67 | 68 | Typically, you want to have something like this in your app, maybe in a layout: 69 | 70 | <% if signed_in? %> 71 | <%= current_user.email %> 72 | <%= link_to "Sign out", sign_out_path, :method => :delete %> 73 | <% else %> 74 | <%= link_to "Sign in", sign_in_path %> 75 | <% end %> 76 | 77 | If you ever want to authenticate the user some place other than sessions/new, 78 | maybe in an API: 79 | 80 | User.authenticate("email@example.com", "password") 81 | 82 | Clearance will deliver one email on your app's behalf: when a user resets their password. Therefore, you should change the default email address that email comes from: 83 | 84 | # config/initializers/clearance.rb 85 | Clearance.configure do |config| 86 | config.mailer_sender = "me@example.com" 87 | end 88 | 89 | Rack 90 | ---- 91 | 92 | Clearance adds its session to the Rack environment hash so middleware and other 93 | Rack applications can interact with it: 94 | 95 | class Bubblegum::Middleware 96 | def initialize(app) 97 | @app = app 98 | end 99 | 100 | def call(env) 101 | if env[:clearance].signed_in? 102 | env[:clearance].current_user.bubble_gum 103 | end 104 | @app.call(env) 105 | end 106 | end 107 | 108 | 109 | Overriding defaults 110 | ------------------- 111 | 112 | Clearance is intended to be small, simple, well-tested, and easy to override defaults. 113 | 114 | Overriding routes 115 | ----------------- 116 | 117 | See [config/routes.rb](https://github.com/thoughtbot/clearance/blob/master/config/routes.rb) for the default behavior. 118 | 119 | To override a Clearance route, redefine it: 120 | 121 | resource :session, :controller => 'sessions' 122 | 123 | Overriding controllers 124 | ---------------------- 125 | 126 | See [app/controllers/clearance](https://github.com/thoughtbot/clearance/tree/master/app/controllers/clearance) for the default behavior. 127 | 128 | To override a Clearance controller, subclass it: 129 | 130 | class SessionsController < Clearance::SessionsController 131 | def new 132 | # my special new action 133 | end 134 | 135 | def url_after_create 136 | my_special_path 137 | end 138 | end 139 | 140 | You may want to override entire actions: 141 | 142 | def new 143 | end 144 | 145 | Or, you may want to override private methods that actions use: 146 | 147 | url_after_create 148 | url_after_update 149 | url_after_destroy 150 | flash_failure_after_create 151 | flash_failure_after_update 152 | flash_failure_when_forbidden 153 | forbid_missing_token 154 | forbid_non_existent_user 155 | 156 | Overriding translations 157 | ----------------------- 158 | 159 | All flash messages and email subject lines are stored in [i18n translations](http://guides.rubyonrails.org/i18n.html). Override them like any other translation. 160 | 161 | Overriding views 162 | ---------------- 163 | 164 | See [app/views](https://github.com/thoughtbot/clearance/tree/master/app/views) for the default behavior. 165 | 166 | To override those **views**, create them in your own `app/views` directory. 167 | 168 | There is a shortcut to copy all Clearance views into your app: 169 | 170 | rails generate clearance:views 171 | 172 | Overriding the model 173 | -------------------- 174 | 175 | If you want to override the **model** behavior, you can include sub-modules of `Clearance::User`: 176 | 177 | extend Clearance::User::ClassMethods 178 | include Clearance::User::Validations 179 | include Clearance::User::Callbacks 180 | 181 | `ClassMethods` contains the `User.authenticate(email, password)` method. 182 | 183 | `Validations` contains validations for email and password. 184 | 185 | `Callbacks` contains `ActiveRecord` callbacks downcasing the email and generating a remember token. 186 | 187 | Overriding the password strategy 188 | -------------------------------- 189 | 190 | By default, Clearance uses SHA1 encryption of the user's password. You can provide your own password strategy by creating a module that conforms to an API of two instance methods: 191 | 192 | def authenticated? 193 | end 194 | 195 | def encrypt_password 196 | end 197 | 198 | See [lib/clearance/password_strategies/sha1.rb](https://github.com/thoughtbot/clearance/blob/master/lib/clearance/password_strategies/sha1.rb) for the default behavior. Also see [lib/clearance/password_strategies/blowfish.rb](https://github.com/thoughtbot/clearance/blob/master/lib/clearance/password_strategies/blowfish.rb) for another password strategy. Switching password strategies will cause your existing users' passwords to not work. 199 | 200 | Once you have an API-compliant module, load it with: 201 | 202 | Clearance.configure do |config| 203 | config.password_strategy = MyPasswordStrategy 204 | end 205 | 206 | For example: 207 | 208 | # default 209 | config.password_strategy = Clearance::PasswordStrategies::SHA1 210 | # Blowfish 211 | config.password_strategy = Clearance::PasswordStrategies::Blowfish 212 | 213 | 214 | Optional Cucumber features 215 | -------------------------- 216 | 217 | Clearance's Cucumber features are dependent on: 218 | 219 | * Cucumber 220 | * Capybara 221 | * RSpec 222 | * Factory Girl 223 | 224 | As your app evolves, you want to know that authentication still works. If you've 225 | installed [Cucumber](http://cukes.info) into your app: 226 | 227 | rails generate cucumber:install 228 | 229 | Then, you can use the Clearance features generator: 230 | 231 | rails generate clearance:features 232 | 233 | Edit your Gemfile to include: 234 | 235 | gem 'factory_girl_rails' 236 | 237 | Edit config/enviroments/test.rb to include the following: 238 | 239 | config.action_mailer.default_url_options = { :host => 'localhost:3000' } 240 | 241 | Then run your tests! 242 | 243 | rake 244 | 245 | Testing 246 | ------- 247 | 248 | If you want to write Rails functional tests or controller specs with Clearance, 249 | you'll need to require the included test helpers and matchers. 250 | 251 | For example, in spec/support/clearance.rb or test/test_helper.rb: 252 | 253 | require 'clearance/testing' 254 | 255 | This will make Clearance::Authentication methods work in your controllers 256 | during functional tests and provide access to helper methods like: 257 | 258 | sign_in 259 | sign_in_as(user) 260 | sign_out 261 | 262 | And matchers like: 263 | 264 | deny_access 265 | 266 | Example: 267 | 268 | context "a visitor" do 269 | before { get :show } 270 | it { should deny_access } 271 | end 272 | 273 | context "a user" do 274 | before do 275 | sign_in 276 | get :show 277 | end 278 | 279 | it { should respond_with(:success) } 280 | end 281 | 282 | Contributing 283 | ------------ 284 | 285 | Please see CONTRIBUTING.md for details. 286 | 287 | Credits 288 | ------- 289 | 290 | ![thoughtbot](http://thoughtbot.com/images/tm/logo.png) 291 | 292 | Clearance is maintained and funded by [thoughtbot, inc](http://thoughtbot.com/community) 293 | 294 | Thank you to all [the contributors](https://github.com/thoughtbot/clearance/contributors)! 295 | 296 | The names and logos for thoughtbot are trademarks of thoughtbot, inc. 297 | 298 | License 299 | ------- 300 | 301 | Clearance is Copyright © 2009-2011 thoughtbot. It is free software, and may be redistributed under the terms specified in the LICENSE file. 302 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | New for 0.16.2: 2 | 3 | * Change default email sender to deploy@example.com . 4 | 5 | New for 0.16.1: 6 | 7 | * Behave correctly when Rails whitelist attributes mass assignment 8 | protection is turned on 9 | * Fix for Rails 3.2.x modifying the HTTP cookie headers in rack requests 10 | 11 | New for 0.16.0: 12 | 13 | * Blowfish password encryption strategy (Chris Dillon) 14 | 15 | New for 0.15.0: 16 | 17 | * The User model can be swapped out using the Clearance.configure method. 18 | * Remove User::InstanceMethods to silence a Rails 3.2 deprecation warning. 19 | * Bump development dependency of cucumber-rails to 1.1.1. 20 | 21 | New for 0.14.0: 22 | 23 | * Support clearance session management from the Rack environment (Joe Ferris) 24 | 25 | New for 0.13.2: 26 | 27 | * Fixed the denies_access matcher (Chad Pytel, Joe Ferris) 28 | 29 | New for 0.13.0: 30 | 31 | * [#170] In Clearance's optional generated features, use pure Capybara instead of depending on Cucumber's removed web_steps, paths, and selectors. (Dan Croak) 32 | * [#167] Extract SHA-1-specific code out of `User` into `PasswordStrategies` module. (Vladimir Andrijevik) 33 | * [#164] Extract sign in form so that other methods can be added easily. (Subhash Chandra) 34 | * [#165] Test against Rails 3.1. (Dan Croak) Required upgrades to Diesel and Appraisal. (Dan Croak, Mike Burns, Chad Pytel) 35 | * [#160] Improved README documentation for overrides. (Dan Croak) 36 | 37 | New for 0.12.0: 38 | 39 | * [#129] Denying access redirects to root_url when signed in, sign_in_url when signed out. (Dan Croak) 40 | * Using flash :notice key everywhere now instead of :success and :failure. More in line with Rails conventions. (Dan Croak) 41 | * [#149] redirect_back_or on sign up. (Dan Croak) 42 | * [#147] Resetting password no longer redirects to sign in page. It displays a message telling them to look for an email. (Dan Croak) 43 | * Removed redundant flash messages. ("Signed in.", "Signed out.", and "You are now signed up.") (Dan Croak) 44 | 45 | New for 0.11.2: 46 | 47 | * Rails 3.1.rc compatible. (Prem Sichanugrist and Dan Croak) 48 | * Cucumber features no longer require password. (Dan Croak) 49 | * No more Clearance shoulda_macros. Instead providing RSpec- and Test::Unit-compliant test matchers (sign_in, sign_in_as, should deny_access, etc). (Dan Croak) 50 | 51 | New for 0.11.1: 52 | 53 | * [#146] Redirect to home page after sign up. (Dan Croak) 54 | * [#145] Remove dependency on dynamic_form. Replaced with flashes due to limited number of failure cases. (Dan Croak) 55 | * Moving ClearanceMailer to app/mailers. Moving spec to spec/mailers. (Dan Croak) 56 | * [#148] Removing :case_sensitive option from validates_uniqueness_of. It was unnecessary and causes a small performance problem on some apps. (Dan Croak) 57 | * Only development dependency in gemspec should be bundler. All others are derived by bundling. (Dan Croak) 58 | 59 | New for 0.11.0: 60 | 61 | * [#141] Removing password confirmation. (Dan Croak) 62 | * [#143] Use ActiveSupport::Concern and ActiveSupport::SecureRandom to clean up code. (Dan Croak) 63 | * New controller#authenticate(params) method. Redefine username & password or other styles of authentication. (Dan Croak) 64 | * before_filter :authenticate API replaced with more aptly-named before_filter :authorize. (Dan Croak) 65 | 66 | New for 0.10.5: 67 | 68 | * Closing CSRF hole for Rails >= 3.0.4 apps (Mack Earnhardt) 69 | 70 | New for 0.10.4: 71 | 72 | * Formtastic views generator removed. (Dan Croak) 73 | * Emails forced to be downcased (particularly for iPhone user case). (Adam Conrad) 74 | * Suite converted from test/unit to RSpec. (Joe Ferris) 75 | * [#135] Password reset requires a password. (Joel Meador) 76 | * [#138] Use HTML5 email fields. (Dan Croak) 77 | 78 | New for 0.10.3.2: 79 | 80 | * Fix gemspec to include all necessary files. 81 | 82 | New for 0.10.3.1: 83 | 84 | * Ensure everything within features inside any engine directory is included in the gemspec 85 | 86 | New for 0.10.3: 87 | 88 | * Include features/engines in gemspec file list so generator works as expected 89 | 90 | New for 0.10.2: 91 | 92 | * Replaced test/rails_root & general testing strategy with Diesel. (Joe Ferris) 93 | * Conveniences in factories for password/confirmation. 94 | * New generator command: rails generate clearance:install. 95 | * Step definitions are now prefixed with visitor_ to use thoughtbot convention. (Dan Croak) 96 | * When Clearance installed in an app that already has users, allow old users to sign in by resetting their password. 97 | 98 | New for 0.10.1: 99 | 100 | * replaced ActionController::Forbidden with a user-friendly flash message. (Dan Croak) 101 | * improved language of Cucumber steps by allowing a little more flexibility. (Dan Croak) 102 | 103 | New for 0.10.0: 104 | 105 | * Lots of README cleanup 106 | * Better email validation regex 107 | * Removed email confirmation step, was mostly a hassle and can always be added back in 108 | at the application level (instead of engine level) if necessary 109 | * Removed disable_with on forms since it does not allow IE users to submit forms. See more: 110 | 111 | https://github.com/rails/jquery-ujs/issues#issue/30 112 | http://bugs.jquery.com/ticket/7061 113 | 114 | New for 0.9.1: 115 | 116 | Forgot to update the changelog in a while, this is going to be brief: 117 | 118 | * This release supports Rails 3, capybara, and shoulda 2.10+. 119 | 120 | New for 0.8.9: 121 | 122 | * Removed unnecessary db index. (Rich Thornett, doctorzaius) 123 | * [#79] Allow customization of cookie duration. (Ron Newman, Dan Croak) 124 | * [#77] rake generator:cleanup needed to be... cleaned up. (Ron Newman) 125 | 126 | New for 0.8.8 (02/25/2010): 127 | 128 | * Fixed sign_in and sign_out not setting current_user (Joe Ferris) 129 | 130 | New for 0.8.7 (02/21/2010): 131 | 132 | * [#43] Fixed global sign out bug. (Ryan McGreary) 133 | * [#69] Allow Rails apps to before_filter :authenticate the entire app 134 | in ApplicationController and still have password recovery work without 135 | overriding any controllers. (Claudio Poli, Dan Croak) 136 | * [#72] #[21] Rails3 fix for ActionController/ActionDispatch change. 137 | (Joseph Holsten, Peter Haza, Dan Croak) 138 | 139 | New for 0.8.6 (02/17/2010): 140 | 141 | * Clearance features capitalization should match view text (Bobby Wilson) 142 | * [#39] skip :authenticate before_filter in controllers so apps can easily 143 | authenticate a whole site without subclassing (Matthew Ford) 144 | * [#45] Added randomness to token and salt generation (Ryan McGeary) 145 | * [#43] Reset the remember_token on sign out instead of sign in. Allows for the same 146 | user to sign in from two locations at once. (Ryan McGeary) 147 | * [#62] Append the version number to generated update migrations (Joe Ferris) 148 | * Allow overridden user models to skip email/password validations 149 | conditionally. This makes username/facebook integration easier. (Joe Ferris) 150 | 151 | New for 0.8.5 (01/20/2010): 152 | 153 | * replaced routing hack with Clearance::Routes.draw(map) to give 154 | more control to the application developer. (Dan Croak) 155 | * removed attr_accessible from Clearance::User. (Dan Croak) 156 | * fixed bug in password reset feature. (Ben Orenstein, Dan Croak) 157 | * use Jeweler for gemming. (Dan Croak) 158 | * remove dependency on root_path, use '/' instead. (Dan Croak) 159 | * use Clearance.configure block to set mailer sender instead of 160 | DO_NOT_REPLY constant. (Dan Croak) 161 | 162 | New for 0.8.4 (12/08/2009): 163 | 164 | * [#48] remove unnecessary require 'factory_girl' in generator (Dan Croak) 165 | * reference gemcutter (not github) as the gem source in README (Dan Croak) 166 | * add IRC, rdoc.info links to README (Dan Croak) 167 | * move user confirmation email trigger into model (Chad Pytel) 168 | 169 | New for 0.8.3 (09/21/2009): 170 | 171 | * [#27] remove class_eval in Clearance::Authentication. (Anuj Dutta) 172 | * Avoid possible collisions in the remember me token (Joe Ferris) 173 | 174 | New for 0.8.2 (09/01/2009): 175 | 176 | * current_user= accessor method. (Joe Ferris, Josh Clayton) 177 | * set current_user in sign_in. (Jon Yurek) 178 | 179 | New for 0.8.1 (08/31/2009): 180 | 181 | * Removed unnecessary remember_token_expires_at column and the 182 | remember? and forget_me! user instance methods. (Dan Croak) 183 | 184 | New for 0.8.0 (08/31/2009): 185 | 186 | * Always remember me. Replaced session-and-remember-me authentication with 187 | always using a cookie with a long timeout. (Dan Croak) 188 | * Documented Clearance::Authentication with YARD. (Dan Croak) 189 | * Documented Clearance::User with YARD. (Dan Croak) 190 | 191 | New for 0.7.0 (08/04/2009): 192 | 193 | * Redirect signed in user who clicks confirmation link again. (Dan Croak) 194 | * Redirect signed out user who clicks confirmation link again. (Dan Croak) 195 | * Added signed_out? convenience method for controllers, helpers, views. (Dan 196 | Croak) 197 | * Added clearance_views generator. By default, creates formtastic views which 198 | pass all tests and features. (Dan Croak) 199 | 200 | New for 0.6.9 (07/04/2009): 201 | 202 | * Added timestamps to create users migration. (Dan Croak) 203 | * Ready for Ruby 1.9. (Jason Morrison, Nick Quaranto) 204 | 205 | New for 0.6.8 (06/24/2009): 206 | 207 | * Added defined? checks for various Rails constants such as ActionController 208 | for easier unit testing of Clearance extensions... particularly ActiveRecord 209 | extensions... particularly strong_password. (Dan Croak) 210 | 211 | New for 0.6.7 (06/13/2009): 212 | 213 | * [#30] Added sign_up, sign_in, sign_out named routes. (Dan Croak) 214 | * [#22] Minimizing Reek smell: Duplication in redirect_back_or. (Dan Croak) 215 | * Deprecated sign_user_in. Told developers to use sign_in instead. (Dan 216 | Croak) 217 | * [#16] flash_success_after_create, flash_notice_after_create, flash_failure_after_create, flash_sucess_after_update, flash_success_after_destroy, etc. (Dan Croak) 218 | * [#17] bug. added #create to forbidden before_filters on confirmations controller. (Dan Croak) 219 | * [#24] should_be_signed_in_as shouldn't look in the session. (Dan Croak) 220 | * README improvements. (Dan Croak) 221 | * Move routes loading to separate file. (Joshua Clayton) 222 | 223 | New for 0.6.6 (05/18/2009): 224 | 225 | * [#14] replaced class_eval in Clearance::User with modules. This was needed 226 | in a thoughtbot client app so we could write our own validations. (Dan Croak) 227 | 228 | New for 0.6.5 (05/17/2009): 229 | 230 | * [#6] Make Clearance i18n aware. (Timur Vafin, Marcel Goerner, Eugene Bolshakov, Dan Croak) 231 | 232 | New for 0.6.4 (05/12/2009): 233 | 234 | * Moved issue tracking to Github from Lighthouse. (Dan Croak) 235 | * [#7] asking higher-level questions of controllers in webrat steps, such as signed_in? instead of what's in the session. same for accessors. (Dan Croak) 236 | * [#11] replacing sign_in_as & sign_out shoulda macros with a stubbing (requires no dependency) approach. this will avoid dealing with the internals of current_user, such as session & cookies. added sign_in macro which signs in an email confirmed user from clearance's factories. (Dan Croak) 237 | * [#13] move private methods on sessions controller into Clearance::Authentication module (Dan Croak) 238 | * [#9] audited flash keys. (Dan Croak) 239 | 240 | New for 0.6.3 (04/23/2009): 241 | 242 | * Scoping ClearanceMailer properly within controllers so it works in production environments. (Nick Quaranto) 243 | 244 | New for 0.6.2 (04/22/2009): 245 | 246 | * Insert Clearance::User into User model if it exists. (Nick Quaranto) 247 | * World(NavigationHelpers) Cucumber 3.0 style. (Shay Arnett & Mark Cornick) 248 | 249 | New for 0.6.1 (04/21/2009): 250 | 251 | * Scope operators are necessary to keep Rails happy. Reverting the original 252 | revert so they're back in the library now for constants referenced inside of 253 | the gem. (Nick Quaranto) 254 | 255 | New for 0.6.0 (04/21/2009): 256 | 257 | * Converted Clearance to a Rails engine. (Dan Croak & Joe Ferris) 258 | * Include Clearance::User in User model in app. (Dan Croak & Joe Ferris) 259 | * Include Clearance::Authentication in ApplicationController. (Dan Croak & Joe Ferris) 260 | * Namespace controllers under Clearance. (Dan Croak & Joe Ferris) 261 | * Routes move to engine, use namespaced controllers but publicly the same. (Dan Croak & Joe Ferris) 262 | * If you want to override a controller, subclass it like SessionsController < 263 | Clearance::SessionsController. This gives you access to usual hooks such as 264 | url_after_create. (Dan Croak & Joe Ferris) 265 | * Controllers, mailer, model, routes all unit tested inside engine. Use 266 | script/generate clearance_features to test integration of Clearance with your 267 | Rails app. No longer including modules in your app's test files. (Dan Croak & Joe Ferris) 268 | * Moved views to engine. (Joe Ferris) 269 | * Converted generated test/factories/clearance.rb to use inheritence for 270 | email_confirmed_user. (Dan Croak) 271 | * Corrected some spelling errors with methods (Nick Quaranto) 272 | * Converted "I should see error messages" to use a regex in the features (Nick 273 | Quaranto) 274 | * Loading clearance routes after rails routes via some monkeypatching (Nick 275 | Quaranto) 276 | * Made the clearance controllers unloadable to stop constant loading errors in 277 | development mode (Nick Quaranto) 278 | 279 | New for 0.5.6 (4/11/2009): 280 | 281 | * [#57] Step definition changed for "User should see error messages" so 282 | features won't fail for certain validations. (Nick Quaranto) 283 | 284 | New for 0.5.5 (3/23/2009): 285 | 286 | * Removing duplicate test to get rid of warning. (Nick Quaranto) 287 | 288 | New for 0.5.4 (3/21/2009): 289 | 290 | * When users fail logging in, redirect them instead of rendering. (Matt 291 | Jankowski) 292 | 293 | New for 0.5.3 (3/5/2009): 294 | 295 | * Clearance now works with (and requires) Shoulda 2.10.0. (Mark Cornick, Joe 296 | Ferris, Dan Croak) 297 | * Prefer flat over nested contexts in sessions_controller_test. (Joe Ferris, 298 | Dan Croak) 299 | 300 | New for 0.5.2 (3/2/2009): 301 | 302 | * Fixed last remaining errors in Rails 2.3 tests. Now fully compatible. (Joe 303 | Ferris, Dan Croak) 304 | 305 | New for 0.5.1 (2/27/2009): 306 | 307 | * [#46] A user with unconfirmed email who resets password now confirms email. 308 | (Marcel Görner) 309 | * Refactored user_from_cookie, user_from_session, User#authenticate to use 310 | more direct return code instead of ugly, harder to read ternary. (Dan Croak) 311 | * Switch order of cookies and sessions to take advantage of Rails 2.3's "Rack-based lazy-loaded sessions":http://is.gd/i23E. (Dan Croak) 312 | * Altered generator to interact with application_controller.rb instead of 313 | application.rb in Rails 2.3 apps. (Dan Croak) 314 | * [#42] Bug fix. Rack-based session change altered how to test remember me 315 | cookie. (Mihai Anca) 316 | 317 | New for 0.5.0 (2/27/2009): 318 | 319 | * Fixed problem with Cucumber features. (Dan Croak) 320 | * Fixed mising HTTP fluency use case. (Dan Croak) 321 | * Refactored User#update_password to take just parameters it needs. (Dan 322 | Croak) 323 | * Refactored User unit tests to be more readable. (Dan Croak) 324 | 325 | New for 0.4.9 (2/20/2009): 326 | 327 | * Protect passwords & confirmations actions with forbidden filters. (Dan Croak) 328 | * Return 403 Forbidden status code in those cases. (Tim Pope) 329 | * Test 403 Forbidden status code in Cucumber feature. (Dan Croak, Joe Ferris) 330 | * Raise custom ActionController::Forbidden error internally. (Joe Ferris, Mike Burns, Jason Morrison) 331 | * Test ActionController::Forbidden error is raised in functional test. (Joe Ferris, Mike Burns, Dan Croak) 332 | * [#45] Fixed bug that allowed anyone to edit another user's password (Marcel Görner) 333 | * Required Factory Girl >= 1.2.0. (Dan Croak) 334 | 335 | New for 0.4.8 (2/16/2009): 336 | 337 | * Added support paths for Cucumber. (Ben Mabey) 338 | * Added documentation for the flash. (Ben Mabey) 339 | * Generators require "test_helper" instead of File.join. for rr compatibility. (Joe Ferris) 340 | * Removed interpolated email address from flash message to make i18n easier. (Bence Nagy) 341 | * Standardized flash messages that refer to email delivery. (Dan Croak) 342 | 343 | New for 0.4.7 (2/12/2009): 344 | 345 | * Removed Clearance::Test::TestHelper so there is one less setup step. (Dan Croak) 346 | * All test helpers now in shoulda_macros. (Dan Croak) 347 | 348 | New for 0.4.6 (2/11/2009): 349 | 350 | * Made the modules behave like mixins again. (hat-tip Eloy Duran) 351 | * Created Actions and PrivateMethods modules on controllers for future RDoc reasons. (Dan Croak, Joe Ferris) 352 | 353 | New for 0.4.5 (2/9/2009): 354 | 355 | * [#43] Removed email downcasing because local-part is case sensitive per RFC5321. (Dan Croak) 356 | * [#42] Removed dependency on Mocha. (Dan Croak) 357 | * Required Shoulda >= 2.9.1. (Dan Croak) 358 | * Added password reset feature to clearance_features generator. (Eugene Bolshakov, Dan Croak) 359 | * Removed unnecessary session[:salt]. (Dan Croak) 360 | * [#41] Only store location for session[:return_to] for GET requests. (Dan Croak) 361 | * Audited "sign up" naming convention. "Register" had slipped in a few places. (Dan Croak) 362 | * Switched to SHA1 encryption. Cypher doesn't matter much for email confirmation, password reset. Better to have shorter hashes in the emails for clients who line break on 72 chars. (Dan Croak) 363 | 364 | New for 0.4.4 (2/2/2009): 365 | 366 | * Added a generator for Cucumber features. (Joe Ferris, Dan Croak) 367 | * Standarized naming for "Sign up," "Sign in," and "Sign out". (Dan Croak) 368 | --------------------------------------------------------------------------------