Skip to content

Commit

Permalink
Release v1.3.3
Browse files Browse the repository at this point in the history
  • Loading branch information
binarylogic committed Nov 23, 2008
1 parent d10f713 commit a623f67
Show file tree
Hide file tree
Showing 16 changed files with 242 additions and 81 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.rdoc
@@ -1,7 +1,13 @@
== 1.3.3 released 2008-11-23

* Updated :act_like_restful_authentication for those using the older version where no site wide key is preset (REST_AUTH_SITE_KEY), Authlogic will adjust automatically based on the presence of this constant.
* Added :transition_from_crypto_provider option for acts_as_authentic to transition your user's passwords to a new algorithm.
* Added :transition_from_restful_authentication for acts_as_authentic to transition your users from restful_authentication to the Authlogic password system. Now you can choose to keep your passwords the same by using :act_like_restful_authentication, which will *NOT* do any transitioning, or you can use :transition_from_crypto_provider which will update your users passwords as they login or new accounts are created, while still allowing users with the old password system to log in.
* Modified the "interface" for the crypto providers to only provide a class level encrypt and matches? method, instead of a class level encrypt and decrypt method.

== 1.3.2 released 2008-11-22

* Updated code to work better with BCrypt, using root level class now.
* Added :act_like_old_restful_authentication option for acts_as_authentic, for those using the older version where no site wide key is preset (REST_AUTH_SITE_KEY)

== 1.3.1 released 2008-11-22

Expand Down
26 changes: 22 additions & 4 deletions README.rdoc
Expand Up @@ -75,6 +75,8 @@ Authlogic makes this a reality. This is just the tip of the ice berg. Keep readi
* <b>Tutorial: Reset passwords with Authlogic the RESTful way:</b> http://www.binarylogic.com/2008/11/16/tutorial-reset-passwords-with-authlogic
* <b>Tutorial: Using OpenID with Authlogic:</b> http://www.binarylogic.com/2008/11/21/tutorial-using-openid-with-authlogic
* <b>Live example of the tutorials above (with source):</b> http://authlogicexample.binarylogic.com
* <b>Tutorial: Easily migrate from restful_authentication:</b> http://www.binarylogic.com/2008/11/23/tutorial-easily-migrate-from-restful_authentication-to-authlogic
* <b>Tutorial: Upgrade passwords easily with Authlogic:</b> http://www.binarylogic.com/2008/11/23/tutorial-upgrade-passwords-easily-with-authlogic
* <b>Bugs / feature suggestions:</b> http://binarylogic.lighthouseapp.com/projects/18752-authlogic

== Install and use
Expand Down Expand Up @@ -197,11 +199,22 @@ For more information on ids checkout Authlogic::Session::Base#id

== Encryption methods

Authlogic is designed so you can use *any* encryption method you want. By default Authlogic uses salted Sha512 with 20 stretches. It also comes preloaded with some other common encryption algorithms so that you can choose. For example, if you wanted to use the BCrypt algorithm just do the following:
Authlogic is designed so you can use *any* encryption method you want. It delegates this task to a class of your choice. By default Authlogic uses salted Sha512 with 20 stretches. It also comes preloaded with some other common encryption algorithms so that you can choose. For example, if you wanted to use the BCrypt algorithm just do the following:

acts_as_authentic :crypto_provider => Authlogic::CryptoProviders::BCrypt

Check out the Authlogic::CryptoProviders module and sublcasses to get an idea of how to write your own crypto provider. It's extremely easy, all that you have to do is make a class with a class level encrypt and optional decrypt method. That's it, the sky is the limit.
For more information on BCrypt checkout my blog post on it: http://www.binarylogic.com/2008/11/22/storing-nuclear-launch-codes-in-your-app-enter-bcrypt-for-authlogic

Also, check out the Authlogic::CryptoProviders module and sublcasses to get an idea of how to write your own crypto provider. It's extremely easy, all that you have to do is make a class with a class level encrypt and matches? method. That's it, the sky is the limit.

== Switching to a new encryption method

Switching to a new encryption method used to be a pain in the ass. Authlogic has an option that makes this dead simple. Let's say you want to migrate to the BCrypt encryption method from Sha512:

acts_as_authentic :crypto_provider => Authlogic::CryptoProviders::BCrypt,
:transition_from_crypto_provider => Authlogic::CryptoProviders::Sha512

That's it. When a user successfully logs in and is using the old method their password will be updated with the new method and all new registrations will use the new method as well. Your users won't know anything changed.

== Tokens (persistence, resetting passwords, private feed access, etc.)

Expand Down Expand Up @@ -382,9 +395,14 @@ Migrating from the restful_authentication plugin? I made an option especially fo
acts_as_authentic :act_like_restful_authentication => true
end

**What's the difference?**
Or you can transition your users to the Authlogic password system:

# app/models/user.rb
class User < ActiveRecord::Base
acts_as_authentic :transition_from_restful_authentication => true
end

restful\_authentication uses Sha1 with 10 stretches to encrypt the password. Authlogic uses Sha512 with 20 stretches. Sha512 is stronger and more secure.
For more information checkout my blog post on this: http://www.binarylogic.com/2008/11/23/tutorial-easily-migrate-from-restful_authentication-to-authlogic

== Framework agnostic (Rails, Merb, etc.)

Expand Down
37 changes: 31 additions & 6 deletions lib/authlogic/crypto_providers/bcrypt.rb
Expand Up @@ -22,7 +22,7 @@ module CryptoProviders
# x.report("Sha512:") { 100.times { Digest::SHA512.hexdigest("mypass") } }
# x.report("Sha1:") { 100.times { Digest::SHA1.hexdigest("mypass") } }
# end

#
# user system total real
# BCrypt (cost = 10): 10.780000 0.060000 10.840000 ( 11.100289)
# BCrypt (cost = 2): 0.180000 0.000000 0.180000 ( 0.181914)
Expand Down Expand Up @@ -50,14 +50,39 @@ def cost
attr_writer :cost

# Creates a BCrypt hash for the password passed.
def encrypt(pass)
::BCrypt::Password.create(pass, :cost => cost)
def encrypt(*tokens)
::BCrypt::Password.create(join_tokens(tokens), :cost => cost)
end

# This does not actually decrypt the password, BCrypt is *not* reversible. The way the bcrypt library is set up requires us to do it this way, which is actually pretty convenient.
def decrypt(crypted_pass)
::BCrypt::Password.new(crypted_pass)
# Does the hash match the tokens? Uses the same tokens that were used to encrypt.
def matches?(hash, *tokens)
hash = new_from_hash(hash)
return false if hash.blank?
hash == join_tokens(tokens)
end

# This method is used as a flag to tell Authlogic to "resave" the password upon a successful login, using the new cost
def cost_matches?(hash)
hash = new_from_hash(hash)
if hash.blank?
false
else
hash.cost == cost
end
end

private
def join_tokens(tokens)
tokens.flatten.join
end

def new_from_hash(hash)
begin
::BCrypt::Password.new(hash)
rescue ::BCrypt::Errors::InvalidHash
return nil
end
end
end
end
end
Expand Down
22 changes: 16 additions & 6 deletions lib/authlogic/crypto_providers/sha1.rb
Expand Up @@ -4,21 +4,31 @@ module Authlogic
module CryptoProviders
# = Sha1
#
# Uses the Sha1 hash algorithm to encrypt passwords. This class is useful if you are migrating from restful_authentication. This uses the
# exact same excryption algorithm with 10 stretches, just like restful_authentication.
# This class was made for the users transitioning from restful_authentication. I highly discourage using this crypto provider as it inferior to your other options.
# Please use the Sha512 crypto provider or the BCrypt provider.
class Sha1
class << self
def join_token
@join_token ||= "--"
end
attr_writer :join_token

# The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to.
def stretches
@stretches ||= 10
end
attr_writer :stretches

# Turns your raw password into a Sha1 hash.
def encrypt(pass)
digest = pass
stretches.times { digest = Digest::SHA1.hexdigest(digest) }
digest
def encrypt(*tokens)
tokens = tokens.flatten
digest = tokens.shift
stretches.times { digest = Digest::SHA1.hexdigest([digest, *tokens].compact.join(join_token)) }
end

# Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
Expand Down
30 changes: 18 additions & 12 deletions lib/authlogic/crypto_providers/sha512.rb
Expand Up @@ -4,22 +4,21 @@ module Authlogic
# = Crypto Providers
#
# The acts_as_authentic method allows you to pass a :crypto_provider option. This allows you to use any type of encryption you like.
# Just create a class with a class level encrypt and decrypt method. The password will be passed as the single parameter to each of these
# methods so you can do your magic.
#
# If you are encrypting via a hash just don't include a decrypt method, since hashes can't be decrypted. Authlogic will notice this adjust accordingly.
# Just create a class with a class level encrypt and matches? method. See example below.
#
# === Example
#
# class MyAwesomeEncryptionMethod
# def self.encrypt(pass)
# # encrypt the pass here
# def self.encrypt(*tokens)
# # the tokens passed wil be an array of objects, what type of object is irrelevant
# # just do what you need to do with them and return a single encrypted string.
# # for example, you will most likely join all of the objects into a single string and then encrypt that string
# end
#
# def self.decrypt(crypted_pass)
# # decrypt the pass here, this is an optional method
# # don't even include this method if you are using a hash algorithm
# # this is irreverisble
# def self.matches?(crypted, *tokens)
# # return true if the crypted string matches the tokens.
# # depending on your algorithm you might decrypt the string then compare it to the token, or you might
# # encrypt the tokens and make sure it matches the crypted string, its up to you
# end
# end
module CryptoProviders
Expand All @@ -28,18 +27,25 @@ module CryptoProviders
# Uses the Sha512 hash algorithm to encrypt passwords.
class Sha512
class << self
attr_accessor :join_token

# The number of times to loop through the encryption. This is ten because that is what restful_authentication defaults to.
def stretches
@stretches ||= 20
end
attr_writer :stretches

# Turns your raw password into a Sha512 hash.
def encrypt(pass)
digest = pass
def encrypt(*tokens)
digest = tokens.flatten.join(join_token)
stretches.times { digest = Digest::SHA512.hexdigest(digest) }
digest
end

# Does the crypted password match the tokens? Uses the same tokens that were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
end
Expand Down
Expand Up @@ -22,15 +22,20 @@ module ActsAsAuthentic
# * <tt>crypto_provider</tt> - default: Authlogic::CryptoProviders::Sha512,
# This is the class that provides your encryption. By default Authlogic provides its own crypto provider that uses Sha512 encrypton.
#
# * <tt>transition_from_crypto_provider</tt> - default: nil,
# This will transition your users to a new encryption algorithm. Let's say you are using Sha1 and you want to transition to Sha512. Just set the
# :crypto_provider option to Authlogic::CryptoProviders::Sha512 and then set this option to Authlogic::CryptoProviders::Sha1. Every time a user
# logs in their password will be resaved with the new algorithm and all new registrations will use the new algorithm as well.
#
# * <tt>act_like_restful_authentication</tt> - default: false,
# If you are migrating from restful_authentication you will want to set this to true, this way your users will still be able to log in and it will seems as
# if nothing has changed. If you don't do this none of your users will be able to log in. If you are starting a new project I do not recommend enabling this
# as the password encryption algorithm used in restful_authentication (Sha1) is not as secure as the one used in authlogic (Sha512). IF you REALLY want to be secure
# checkout Authlogic::CryptoProviders::BCrypt.
#
# * <tt>act_like_old_restful_authentication</tt> - default: false,
# This is the same thing as :act_like_restful_authentication, but it is for the older versions. How can you tell if you are using an older version? The new version
# requires that you supply a site wide key called REST_AUTH_SITE_KEY. If you do not have that in your project then you are using the older version.
# * <tt>transition_from_restful_authentication</tt> - default: false,
# This works just like :transition_from_crypto_provider, but it makes some special exceptions so that your users will transition from restful_authentication, since
# restful_authentication does things a little different than Authlogic.
#
# * <tt>login_field</tt> - default: :login, :username, or :email, depending on which column is present, if none are present defaults to :login
# The name of the field used for logging in. Only specify if you aren't using any of the defaults.
Expand Down Expand Up @@ -194,11 +199,12 @@ def acts_as_authentic_with_config(options = {})
options[:email_field_validates_uniqueness_of_options][:scope] ||= options[:scope]
end

if options[:act_like_restful_authentication] || options[:act_like_old_restful_authentication]
options[:crypto_provider] = CryptoProviders::Sha1
if options[:act_like_old_restful_authentication]
const_set("::REST_AUTH_SITE_KEY", nil) unless defined?(REST_AUTH_SITE_KEY)
options[:crypto_provider].stretches = 1
if options[:act_like_restful_authentication] || options[:transition_from_restful_authentication]
crypto_provider_key = options[:act_like_restful_authentication] ? :crypto_provider : :transition_from_crypto_provider
options[crypto_provider_key] = CryptoProviders::Sha1
if !defined?(REST_AUTH_SITE_KEY) || REST_AUTH_SITE_KEY.nil?
class_eval("::REST_AUTH_SITE_KEY = nil") unless defined?(REST_AUTH_SITE_KEY)
options[crypto_provider_key].stretches = 1
end
end

Expand Down
Expand Up @@ -53,6 +53,7 @@ def acts_as_authentic_with_credentials(options = {})
end

attr_reader options[:password_field]
attr_accessor :crypto_provider

class_eval <<-"end_eval", __FILE__, __LINE__
def self.friendly_unique_token
Expand All @@ -66,13 +67,37 @@ def #{options[:password_field]}=(pass)
return if pass.blank?
@#{options[:password_field]} = pass
self.#{options[:password_salt_field]} = self.class.unique_token
self.#{options[:crypted_password_field]} = #{options[:crypto_provider]}.encrypt(obfuscate_password(@#{options[:password_field]}))
self.#{options[:crypted_password_field]} = #{options[:crypto_provider]}.encrypt(*encrypt_arguments(@#{options[:password_field]}, #{options[:act_like_restful_authentication].inspect} ? :restful_authentication : nil))
end
alias_method :update_#{options[:password_field]}, :#{options[:password_field]}= # this is to avoids the method chain, so we are ONLY changing the password
def valid_#{options[:password_field]}?(attempted_password)
return false if attempted_password.blank? || #{options[:crypted_password_field]}.blank? || #{options[:password_salt_field]}.blank?
(#{options[:crypto_provider]}.respond_to?(:decrypt) && #{options[:crypto_provider]}.decrypt(#{options[:crypted_password_field]}) == attempted_password + #{options[:password_salt_field]}) ||
(!#{options[:crypto_provider]}.respond_to?(:decrypt) && #{options[:crypto_provider]}.encrypt(obfuscate_password(attempted_password)) == #{options[:crypted_password_field]})
[#{options[:crypto_provider]}, #{options[:transition_from_crypto_provider].inspect}].compact.each do |encryptor|
# The arguments_type of for the transitioning from restful_authentication
arguments_type = nil
case encryptor
when #{options[:crypto_provider]}
arguments_type = :restful_authentication if #{options[:act_like_restful_authentication].inspect}
when #{options[:transition_from_crypto_provider].inspect}
arguments_type = :restful_authentication if #{options[:transition_from_restful_authentication].inspect}
end
if encryptor.matches?(#{options[:crypted_password_field]}, *encrypt_arguments(attempted_password, arguments_type))
# If we are transitioning from an older encryption algorithm and the password is still using the old algorithm
# then let's reset the password using the new algorithm. If the algorithm has a cost (BCrypt) and the cost has changed, update the password with
# the new cost.
if encryptor == #{options[:transition_from_crypto_provider].inspect} || (encryptor.respond_to?(:cost_matches?) && !encryptor.cost_matches?(#{options[:crypted_password_field]}))
update_#{options[:password_field]}(attempted_password)
save(false)
end
return true
end
end
false
end
def reset_#{options[:password_field]}
Expand All @@ -82,18 +107,24 @@ def reset_#{options[:password_field]}
end
alias_method :randomize_password, :reset_password
def confirm_#{options[:password_field]}
raise "confirm_#{options[:password_field]} has been removed, please use #{options[:password_field]}_confirmation. " +
"As this is the field that ActiveRecord automatically creates with validates_confirmation_of."
end
def reset_#{options[:password_field]}!
reset_#{options[:password_field]}
save_without_session_maintenance(false)
end
alias_method :randomize_password!, :reset_password!
private
def obfuscate_password(raw_password)
if #{options[:act_like_restful_authentication].inspect} || #{options[:act_like_old_restful_authentication].inspect}
[REST_AUTH_SITE_KEY, raw_password, #{options[:password_salt_field]}, REST_AUTH_SITE_KEY].compact.join("--")
def encrypt_arguments(raw_password, arguments_type = nil)
case arguments_type
when :restful_authentication
[REST_AUTH_SITE_KEY, raw_password, #{options[:password_salt_field]}, REST_AUTH_SITE_KEY]
else
raw_password + #{options[:password_salt_field]}
[raw_password, #{options[:password_salt_field]}]
end
end
end_eval
Expand Down

0 comments on commit a623f67

Please sign in to comment.