Permalink
Browse files

BCrypt for passwords

This commit makes BCrypt the default for new setups, and introduces a
strategy for converting existing infrastructure to BCrypt.

To switch to BCrypt now:

    Clearance.configure do |config|
      config.password_strategy = Clearance::PasswordStrategies::BCrypt
    end

To set the password strategy to the conversion layer:

    Clearance.configure do |config|
      config.password_strategy = Clearance::PasswordStrategies::BCryptMigrationFromSHA1
    end

To continue to use SHA1:

    Clearance.configure do |config|
      config.password_strategy = Clearance::PasswordStrategies::SHA1
    end
  • Loading branch information...
1 parent 3746806 commit be37c354594a33333006dcdff38ffc46bdc55620 Dan Croak and Gabe Berke-Williams committed with mike-burns Oct 23, 2011
View
@@ -2,6 +2,7 @@ PATH
remote: .
specs:
clearance (0.16.2)
+ bcrypt-ruby
diesel (~> 0.1.5)
rails (>= 3.0)
@@ -45,6 +46,7 @@ GEM
cucumber (>= 1.1.1)
ffi (>= 1.0.11)
rspec (>= 2.7.0)
+ bcrypt-ruby (3.0.1)
bourne (1.1.2)
mocha (= 0.10.5)
builder (3.0.0)
View
@@ -1,3 +1,9 @@
+HEAD:
+
+* Change default password strategy to BCrypt
+* Provide BCryptMigrationFromSHA1 password strategy to help people migrate from
+ SHA1 (the old default password strategy) to BCrypt (the new default).
+
New for 0.16.2:
* Change default email sender to deploy@example.com .
View
@@ -188,15 +188,27 @@ If you want to override the **model** behavior, you can include sub-modules of `
Overriding the password strategy
--------------------------------
-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:
+By default, Clearance uses BCrypt 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:
def authenticated?
end
- def encrypt_password
+ def password=(new_password)
end
-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.
+The previous default password strategy was SHA1. To keep using SHA1, use this
+code:
+
+ Clearance.configure do |config|
+ config.password_strategy = Clearance::PasswordStrategies::SHA1
+ end
+
+See [lib/clearance/password_strategies/bcrypt.rb](https://github.com/thoughtbot/clearance/blob/master/lib/clearance/password_strategies/bcrypt.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. If you are currently using the SHA1 strategy (the previous default), and
+want to transparently switch to BCrypt, use the [BCryptMigrationFromSHA1 strategy](https://github.com/thoughtbot/clearance/blob/master/lib/clearance/password_strategies/bcrypt_migration_from_sha1.rb).
+
Once you have an API-compliant module, load it with:
@@ -207,11 +219,14 @@ Once you have an API-compliant module, load it with:
For example:
# default
+ config.password_strategy = Clearance::PasswordStrategies::BCrypt
+ # use this strategy if you used to use SHA1, and now you want to use BCrypt
+ config.password_strategy = Clearance::PasswordStrategies::BCryptMigrationFromSHA1
+ # SHA1 (the previous default)
config.password_strategy = Clearance::PasswordStrategies::SHA1
# Blowfish
config.password_strategy = Clearance::PasswordStrategies::Blowfish
-
Routing Constraints
-------------------
View
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
s.add_dependency('rails', '>= 3.0')
s.add_dependency('diesel', '~> 0.1.5')
+ s.add_dependency('bcrypt-ruby')
s.add_development_dependency('bundler', '~> 1.1')
s.add_development_dependency('appraisal', '~> 0.4.1')
@@ -33,7 +33,7 @@ Feature: Password reset
Then I should be signed in
Scenario: User who was created before Clearance was installed creates password for first time
- Given a user "email@example.com" exists without a salt, remember token, or password
+ Given a user "email@example.com" exists without a remember token or password
When I reset the password for "email@example.com"
When I follow the password reset link sent to "email@example.com"
And I update my password with "newpassword"
@@ -6,9 +6,9 @@
FactoryGirl.create(:user, :email => email)
end
-Given /^a user "([^"]*)" exists without a salt, remember token, or password$/ do |email|
+Given /^a user "([^"]*)" exists without a remember token or password$/ do |email|
user = FactoryGirl.create(:user, :email => email)
- sql = "update users set salt = NULL, encrypted_password = NULL, remember_token = NULL where id = #{user.id}"
+ sql = "update users set encrypted_password = NULL, remember_token = NULL where id = #{user.id}"
ActiveRecord::Base.connection.update(sql)
end
@@ -1,7 +1,8 @@
PATH
- remote: /home/mike/clearance
+ remote: /Users/gabe/thoughtbot/open-source/clearance
specs:
clearance (0.16.2)
+ bcrypt-ruby
diesel (~> 0.1.5)
rails (>= 3.0)
@@ -45,6 +46,7 @@ GEM
cucumber (>= 1.1.1)
ffi (>= 1.0.11)
rspec (>= 2.7.0)
+ bcrypt-ruby (3.0.1)
bourne (1.1.2)
mocha (= 0.10.5)
builder (2.1.2)
@@ -2,6 +2,7 @@ PATH
remote: /Users/gabe/thoughtbot/open-source/clearance
specs:
clearance (0.16.2)
+ bcrypt-ruby
diesel (~> 0.1.5)
rails (>= 3.0)
@@ -46,6 +47,7 @@ GEM
cucumber (>= 1.1.1)
ffi (>= 1.0.11)
rspec (>= 2.7.0)
+ bcrypt-ruby (3.0.1)
bourne (1.1.2)
mocha (= 0.10.5)
builder (3.0.0)
@@ -2,6 +2,7 @@ PATH
remote: /Users/gabe/thoughtbot/open-source/clearance
specs:
clearance (0.16.2)
+ bcrypt-ruby
diesel (~> 0.1.5)
rails (>= 3.0)
@@ -45,6 +46,7 @@ GEM
cucumber (>= 1.1.1)
ffi (>= 1.0.11)
rspec (>= 2.7.0)
+ bcrypt-ruby (3.0.1)
bourne (1.1.2)
mocha (= 0.10.5)
builder (3.0.0)
@@ -2,5 +2,8 @@ module Clearance
module PasswordStrategies
autoload :SHA1, 'clearance/password_strategies/sha1'
autoload :Blowfish, 'clearance/password_strategies/blowfish'
+ autoload :BCrypt, 'clearance/password_strategies/bcrypt'
+ autoload :Fake, 'clearance/password_strategies/fake'
+ autoload :BCryptMigrationFromSHA1, 'clearance/password_strategies/bcrypt_migration_from_sha1'
end
end
@@ -0,0 +1,32 @@
+module Clearance
+ module PasswordStrategies
+ module BCrypt
+ require 'bcrypt'
+
+ extend ActiveSupport::Concern
+
+ # Am I authenticated with given password?
+ #
+ # @param [String] plain-text password
+ # @return [true, false]
+ # @example
+ # user.authenticated?('password')
+ def authenticated?(password)
+ ::BCrypt::Password.new(encrypted_password) == password
+ end
+
+ def password=(new_password)
+ @password = new_password
+ if new_password.present?
+ self.encrypted_password = encrypt(new_password)
+ end
+ end
+
+ private
+
+ def encrypt(password)
+ ::BCrypt::Password.create(password)
+ end
+ end
+ end
+end
@@ -0,0 +1,52 @@
+module Clearance
+ module PasswordStrategies
+ module BCryptMigrationFromSHA1
+ class BCryptUser
+ include Clearance::PasswordStrategies::BCrypt
+
+ def initialize(user)
+ @user = user
+ end
+
+ delegate :encrypted_password, :encrypted_password=, to: :@user
+ end
+
+ class SHA1User
+ include Clearance::PasswordStrategies::SHA1
+
+ def initialize(user)
+ @user = user
+ end
+
+ delegate :salt, :salt=, :encrypted_password, :encrypted_password=, to: :@user
+ end
+
+ def authenticated?(password)
+ authenticated_with_sha1?(password) || authenticated_with_bcrypt?(password)
+ end
+
+ def password=(new_password)
+ BCryptUser.new(self).password = new_password
+ end
+
+ private
+
+ def authenticated_with_sha1?(password)
+ if sha1_password?
+ if SHA1User.new(self).authenticated?(password)
+ self.password = password
+ true
+ end
+ end
+ end
+
+ def authenticated_with_bcrypt?(password)
+ BCryptUser.new(self).authenticated?(password)
+ end
+
+ def sha1_password?
+ self.encrypted_password =~ /^[a-f0-9]{40}$/
+ end
+ end
+ end
+end
@@ -13,15 +13,16 @@ def authenticated?(password)
encrypted_password == encrypt(password)
end
- protected
-
- def encrypt_password
+ def password=(new_password)
+ @password = new_password
initialize_salt_if_necessary
- if password.present?
- self.encrypted_password = encrypt(password)
+ if new_password.present?
+ self.encrypted_password = encrypt(new_password)
end
end
+ protected
+
def generate_hash(string)
cipher = OpenSSL::Cipher::Cipher.new('bf-cbc').encrypt
cipher.key = Digest::SHA256.digest(salt)
@@ -0,0 +1,34 @@
+module Clearance
+ module PasswordStrategies
+ # The Clearance::PasswordStrategies::Fake module is meant to be used in test suites.
+ # It stores passwords as plain text so your test suite doesn't pay the time cost
+ # of any hashing algorithm.
+ #
+ # Use the fake in your test suite by requiring Clearance's testing helpers:
+ #
+ # require 'clearance/testing'
+ #
+ # The usual places you'd require it are:
+ #
+ # spec/support/clearance.rb
+ # features/support/clearance.rb
+ module Fake
+ extend ActiveSupport::Concern
+
+ def authenticated?(password)
+ encrypted_password == password
+ end
+
+ def password=(new_password)
+ @password = new_password
+ if new_password.present?
+ self.encrypted_password = encrypt(password)
+ end
+ end
+
+ def encrypt(password)
+ password
+ end
+ end
+ end
+end
@@ -1,8 +1,10 @@
-require 'digest/sha1'
-
module Clearance
module PasswordStrategies
module SHA1
+ require 'digest/sha1'
+
+ extend ActiveSupport::Concern
+
# Am I authenticated with given password?
#
# @param [String] plain-text password
@@ -13,15 +15,16 @@ def authenticated?(password)
encrypted_password == encrypt(password)
end
- protected
-
- def encrypt_password
+ def password=(new_password)
+ @password = new_password
initialize_salt_if_necessary
- if password.present?
- self.encrypted_password = encrypt(password)
+ if new_password.present?
+ self.encrypted_password = encrypt(new_password)
end
end
+ private
+
def generate_hash(string)
if RUBY_VERSION >= '1.9'
Digest::SHA1.hexdigest(string).encode('UTF-8')
@@ -2,6 +2,10 @@
require 'clearance/testing/deny_access_matcher'
require 'clearance/testing/helpers'
+Clearance.configure do |config|
+ config.password_strategy = Clearance::PasswordStrategies::Fake
+end
+
if defined?(ActionController::TestCase)
ActionController::TestCase.extend Clearance::Testing::Matchers
class ActionController::TestCase
View
@@ -16,12 +16,12 @@ module User
# @see Callbacks
included do
attr_accessor :password_changing
- attr_reader :password
+ attr_reader :password
include Validations
include Callbacks
- include (Clearance.configuration.password_strategy || Clearance::PasswordStrategies::SHA1)
+ include (Clearance.configuration.password_strategy || Clearance::PasswordStrategies::BCrypt)
end
module ClassMethods
@@ -63,7 +63,7 @@ module Callbacks
# salt, token, password encryption are handled before_save.
included do
before_validation :downcase_email
- before_create :generate_remember_token
+ before_create :generate_remember_token
end
end
@@ -108,12 +108,7 @@ def update_password(new_password)
save
end
- def password=(unencrypted_password)
- @password = unencrypted_password
- encrypt_password
- end
-
- protected
+ private
def generate_random_code(length = 20)
if RUBY_VERSION >= '1.9'
@@ -5,7 +5,6 @@ def self.up
columns = [
[:email, 't.string :email'],
[:encrypted_password, 't.string :encrypted_password, :limit => 128'],
- [:salt, 't.string :salt, :limit => 128'],
[:confirmation_token, 't.string :confirmation_token, :limit => 128'],
[:remember_token, 't.string :remember_token, :limit => 128']
].delete_if {|c| existing_columns.include?(c.first.to_s)}
Oops, something went wrong.

0 comments on commit be37c35

Please sign in to comment.