Skip to content

Commit

Permalink
BCrypt for passwords
Browse files Browse the repository at this point in the history
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
Dan Croak and Gabe Berke-Williams authored and mike-burns committed Jun 29, 2012
1 parent 3746806 commit be37c35
Show file tree
Hide file tree
Showing 25 changed files with 334 additions and 61 deletions.
2 changes: 2 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ PATH
remote: .
specs:
clearance (0.16.2)
bcrypt-ruby
diesel (~> 0.1.5)
rails (>= 3.0)

Expand Down Expand Up @@ -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)
Expand Down
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -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 .
Expand Down
23 changes: 19 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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
-------------------

Expand Down
1 change: 1 addition & 0 deletions clearance.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 1 addition & 1 deletion features/engine/visitor_resets_password.feature
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
4 changes: 2 additions & 2 deletions features/step_definitions/engine/clearance_steps.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 3 additions & 1 deletion gemfiles/3.0.12.gemfile.lock
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions gemfiles/3.1.4.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions gemfiles/3.2.3.gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/clearance/password_strategies.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 32 additions & 0 deletions lib/clearance/password_strategies/bcrypt.rb
Original file line number Diff line number Diff line change
@@ -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
52 changes: 52 additions & 0 deletions lib/clearance/password_strategies/bcrypt_migration_from_sha1.rb
Original file line number Diff line number Diff line change
@@ -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
11 changes: 6 additions & 5 deletions lib/clearance/password_strategies/blowfish.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
34 changes: 34 additions & 0 deletions lib/clearance/password_strategies/fake.rb
Original file line number Diff line number Diff line change
@@ -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
17 changes: 10 additions & 7 deletions lib/clearance/password_strategies/sha1.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions lib/clearance/testing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 4 additions & 9 deletions lib/clearance/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)}
Expand Down
Loading

0 comments on commit be37c35

Please sign in to comment.