Skip to content

Instantly share code, notes, and snippets.

@spullen
Created November 8, 2012 16:46
Show Gist options
  • Save spullen/4039991 to your computer and use it in GitHub Desktop.
Save spullen/4039991 to your computer and use it in GitHub Desktop.
HybridAuthenticatable
# db/migrate/<timestamp>_devise_create_user.rb
class DeviseCreateUsers < ActiveRecord::Migration
def change
create_table(:users) do |t|
## Database authenticatable
t.string :username, :null => false, :default => ""
t.string :email, :null => false, :default => ""
t.string :encrypted_password, :null => false, :default => ""
## Recoverable
t.string :reset_password_token
t.datetime :reset_password_sent_at
## Rememberable
t.datetime :remember_created_at
## Trackable
t.integer :sign_in_count, :default => 0
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string :current_sign_in_ip
t.string :last_sign_in_ip
## Confirmable
# t.string :confirmation_token
# t.datetime :confirmed_at
# t.datetime :confirmation_sent_at
# t.string :unconfirmed_email # Only if using reconfirmable
## Lockable
# t.integer :failed_attempts, :default => 0 # Only if lock strategy is :failed_attempts
# t.string :unlock_token # Only if unlock strategy is :email or :both
# t.datetime :locked_at
## Token authenticatable
# t.string :authentication_token
t.string :override_ldap, :default => false
t.timestamps
end
add_index :users, :username, :unique => true
add_index :users, :reset_password_token, :unique => true
# add_index :users, :confirmation_token, :unique => true
# add_index :users, :unlock_token, :unique => true
# add_index :users, :authentication_token, :unique => true
end
end
# config/initializers/hybrid_authenticatable.rb
require 'devise/strategies/authenticatable'
require 'bcrypt'
module Devise
module Strategies
# Strategy for signing in a user based on his login and password using LDAP.
# Redirects to sign_in page if it's not authenticated
class HybridAuthenticatable < Authenticatable
# Authenticate a user based on login and password params, returning to warden
# success and the authenticated user if everything is okay. Otherwise redirect
# to sign in page.
def authenticate!
resource = User.find_by_username(params[:user][:username])
return fail(:invalid) if resource.nil? || !valid_password?
if resource.override_ldap == true
puts 'Authentication by Database'
resource = mapping.to.find_for_database_authentication(authentication_hash)
if validate(resource)
resource.after_database_authentication
success!(resource)
end
else
puts 'Authentication by LDAP'
resource = mapping.to.authenticate_with_ldap(params[scope])
if validate(resource)
resource.after_database_authentication
success!(resource)
else
fail(:invalid)
end
end
end
end
end
module Models
module HybridAuthenticatable
extend ActiveSupport::Concern
included do
attr_reader :password, :current_password
attr_accessor :password_confirmation
end
def self.required_fields(klass)
[:encrypted_password] + klass.authentication_keys
end
# Generates password encryption based on the given value.
def password=(new_password)
@password = new_password
self.encrypted_password = password_digest(@password) if self.override_ldap && @password.present?
end
def login_with
@login_with ||= Devise.mappings[self.class.to_s.underscore.to_sym].to.authentication_keys.first
self[@login_with]
end
def change_password!(current_password)
raise "Need to set new password first" if @password.blank?
Devise::LdapAdapter.update_own_password(login_with, @password, current_password)
end
def reset_password!(new_password, new_password_confirmation)
if new_password == new_password_confirmation && ::Devise.ldap_update_password
Devise::LdapAdapter.update_password(login_with, new_password)
end
clear_reset_password_token if valid?
save
end
# Checks if a resource is valid upon authentication.
def valid_ldap_authentication?(password)
if Devise::LdapAdapter.valid_credentials?(login_with, password)
return true
else
return false
end
end
def ldap_groups
Devise::LdapAdapter.get_groups(login_with)
end
def ldap_dn
Devise::LdapAdapter.get_dn(login_with)
end
def ldap_get_param(login_with, param)
Devise::LdapAdapter.get_ldap_param(login_with,param)
end
# Verifies whether an password (ie from sign in) is the user password.
def valid_password?(password)
if override_ldap
return false if encrypted_password.blank?
bcrypt = ::BCrypt::Password.new(encrypted_password)
password = ::BCrypt::Engine.hash_secret("#{password}#{self.class.pepper}", bcrypt.salt)
Devise.secure_compare(password, encrypted_password)
else
return true
end
end
# Set password and password confirmation to nil
def clean_up_passwords
self.password = self.password_confirmation = nil
end
# Update record attributes when :current_password matches, otherwise returns
# error on :current_password. It also automatically rejects :password and
# :password_confirmation if they are blank.
def update_with_password(params, *options)
current_password = params.delete(:current_password)
if params[:password].blank?
params.delete(:password)
params.delete(:password_confirmation) if params[:password_confirmation].blank?
end
result = if valid_password?(current_password)
update_attributes(params, *options)
else
params.delete(:password)
self.assign_attributes(params, *options)
self.valid?
self.errors.add(:current_password, current_password.blank? ? :blank : :invalid)
false
end
clean_up_passwords
result
end
# Updates record attributes without asking for the current password.
# Never allows to change the current password. If you are using this
# method, you should probably override this method to protect other
# attributes you would not like to be updated without a password.
#
# Example:
#
# def update_without_password(params={})
# params.delete(:email)
# super(params)
# end
#
def update_without_password(params, *options)
params.delete(:password)
params.delete(:password_confirmation)
result = update_attributes(params, *options)
clean_up_passwords
result
end
def after_database_authentication
end
# A reliable way to expose the salt regardless of the implementation.
def authenticatable_salt
encrypted_password[0,29] if encrypted_password
end
protected
# Digests the password using bcrypt.
def password_digest(password)
::BCrypt::Password.create("#{password}#{self.class.pepper}", :cost => self.class.stretches).to_s
end
module ClassMethods
Devise::Models.config(self, :pepper, :stretches)
# We assume this method already gets the sanitized values from the
# DatabaseAuthenticatable strategy. If you are using this method on
# your own, be sure to sanitize the conditions hash to only include
# the proper fields.
def find_for_database_authentication(conditions)
find_for_authentication(conditions)
end
# Authenticate a user based on configured attribute keys. Returns the
# authenticated user if it's valid or nil.
def authenticate_with_ldap(attributes={})
auth_key = self.authentication_keys.first
return nil unless attributes[auth_key].present?
auth_key_value = (self.case_insensitive_keys || []).include?(auth_key) ? attributes[auth_key].downcase : attributes[auth_key]
# resource = find_for_ldap_authentication(conditions)
resource = where(auth_key => auth_key_value).first
if (resource.blank? and ::Devise.ldap_create_user)
resource = new
resource[auth_key] = auth_key_value
resource.password = attributes[:password]
end
if resource.try(:valid_ldap_authentication?, attributes[:password])
if resource.new_record?
resource.ldap_before_save if resource.respond_to?(:ldap_before_save)
resource.save
end
return resource
else
return nil
end
end
end
end
end
end
Warden::Strategies.add(:hybrid_authenticatable, Devise::Strategies::HybridAuthenticatable)
# app/models/user.rb
class User < ActiveRecord::Base
devise :hybrid_authenticatable, :database_authenticatable, :ldap_authenticatable, :rememberable, :trackable, :validatable
attr_accessible :username, :email, :password, :password_confirmation, :remember_me, :override_ldap
end
@spullen
Copy link
Author

spullen commented Nov 8, 2012

module TestMod
  def self.included(base)
    base.extend(ClassMethods)
  end
end

@spullen
Copy link
Author

spullen commented Nov 8, 2012

It'd help if the code sample was complete...

module TestMod
  def self.included(base)
    base.extend(ClassMethods)
  end

  module ClassMethods
    def say_hello
      puts 'HELLO'
    end

    def say_goodbye
      puts 'GOODBYE'
    end
  end
end

@spullen
Copy link
Author

spullen commented Nov 8, 2012

module TestMod
  def say_hello
    puts 'HELLO!'
  end

  def say_goodbye
    puts 'GOODBYE!'
  end
end

class MyTest
end

my_test = MyTest.new
my_test.extend(TestMod::ClassMethods)

my_test.say_hello
my_test.say_goodbye

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment