Skip to content
This repository was archived by the owner on Nov 9, 2017. It is now read-only.

Token-Based Authentication #9

Open
wants to merge 13 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ end

group :test do
gem 'sqlite3'
end
end
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,20 @@ Yes, you can do that too. Let's assume you also want to authenticate admins that

Bam! You're done. Now you have an AdminSession object that will use *username* and *password* to authenticate.

Token-Based Authentication
==========================
Token authentication provides a simple solution for exposing APIs. When enabled, Letmein will automatically generate a token (40 character hex) for each new user. Instead of passing *email* and *password* to the *UserSession*, you merely pass *auth_token*. To enable tokens:

LetMeIn.configure do |conf|
conf.generate_token = true
end

Its usage differs from the *email/password* combo in only one way:

@session = UserSession.new(:auth_token => "258082e5588dea110592154f48ef1e309a8bbff5")

This will successfully (or not) authenticate you with the token above.

Overriding Session Authentication
=================================
By default user will be logged in if provided email and password match. If you need to add a bit more logic to that you'll need to create your own session object. In the following example we do an additional check to see if user is 'approved' before letting him in.
Expand Down
63 changes: 53 additions & 10 deletions lib/letmein.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'active_record'
require 'securerandom'
require 'bcrypt'

module LetMeIn
Expand All @@ -17,13 +18,15 @@ class Railtie < Rails::Railtie
# conf.identifier = 'username'
# end
class Config
ACCESSORS = %w(models attributes passwords salts)
ACCESSORS = %w(models attributes passwords salts tokens generate_tokens)
attr_accessor *ACCESSORS
def initialize
@models = ['User']
@attributes = ['email']
@passwords = ['password_hash']
@salts = ['password_salt']
@models = ['User']
@attributes = ['email']
@passwords = ['password_hash']
@salts = ['password_salt']
@tokens = ['auth_token']
@generate_tokens = [false]
end
ACCESSORS.each do |a|
define_method("#{a.singularize}=") do |val|
Expand All @@ -48,6 +51,7 @@ class << self

attr_accessor :login, # [email protected]
:password, # secretpassword
:token, # 40 char, hex, single token authentication
:object # authenticated object

validate :authenticate
Expand All @@ -59,6 +63,7 @@ def initialize(params = { })
self.class.attribute ||= LetMeIn.accessor(:attribute, LetMeIn.config.models.index(self.class.model))
self.login = params[:login] || params[self.class.attribute.to_sym]
self.password = params[:password]
self.token = params[self.token_attribute.to_sym] if allow_tokens?
end

def save
Expand Down Expand Up @@ -87,11 +92,18 @@ def method_missing(method_name, *args)
end

def authenticate
p = LetMeIn.accessor(:password, LetMeIn.config.models.index(self.class.model))
s = LetMeIn.accessor(:salt, LetMeIn.config.models.index(self.class.model))
unless self.token
p = LetMeIn.accessor(:password, LetMeIn.config.models.index(self.class.model))
s = LetMeIn.accessor(:salt, LetMeIn.config.models.index(self.class.model))

object = self.class.model.constantize.where(self.class.attribute => self.login).first
self.object = object if object && !object.send(p).blank? && object.send(p) == BCrypt::Engine.hash_secret(self.password, object.send(s))
else
self.object = self.class.model.constantize.where(self.token_attribute => self.token).first
end

object = self.class.model.constantize.where("#{self.class.attribute}" => self.login).first
self.object = if object && !object.send(p).blank? && object.send(p) == BCrypt::Engine.hash_secret(self.password, object.send(s))
if self.object
self.token = self.object.send(self.token_attribute) if allow_tokens?
object
else
errors.add :base, 'Failed to authenticate'
Expand All @@ -102,22 +114,53 @@ def authenticate
def to_key
nil
end

protected

def token_attribute
LetMeIn.accessor(:token, LetMeIn.config.models.index(self.class.model))
end

def allow_tokens?
LetMeIn.config.generate_tokens[LetMeIn.config.models.index(self.class.model)] ? true : false
end

end

module Model
def self.included(base)
base.instance_eval do
attr_accessor :password
before_save :encrypt_password
before_save :generate_token

define_method :encrypt_password do
unless LetMeIn.config.models.index(self.class.to_s)
raise(LetMeIn::Error, "#{self.class.to_s} must be added to your LetMeIn initializer")
end

if password.present?
p = LetMeIn.accessor(:password, LetMeIn.config.models.index(self.class.to_s))
s = LetMeIn.accessor(:salt, LetMeIn.config.models.index(self.class.to_s))
self.send("#{s}=", BCrypt::Engine.generate_salt)
self.send("#{p}=", BCrypt::Engine.hash_secret(password, self.send(s)))
end
end

define_method :generate_token do
i = LetMeIn.config.models.index(self.class.to_s)
if LetMeIn.config.generate_tokens[i]
t = LetMeIn.accessor(:token, i)
unless self.send(t).present?
token = nil
loop do
token = SecureRandom.hex(20)
break token unless base.where(t => token).exists?
end
self.send("#{t}=", token)
end
end
end
end
end
end
Expand Down Expand Up @@ -147,4 +190,4 @@ def self.accessor(name, index = 0)
Object.const_set(session_model, Class.new(LetMeIn::Session))
end
end
end
end
123 changes: 113 additions & 10 deletions test/letmein_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
ActiveRecord::Base.logger = Logger.new($stdout)

class User < ActiveRecord::Base ; end
class SubUser < User ; end
class Admin < ActiveRecord::Base ; end

class OpenSession < LetMeIn::Session
Expand Down Expand Up @@ -40,11 +41,13 @@ def setup
t.column :email, :string
t.column :password_hash, :string
t.column :password_salt, :string
t.column :auth_token, :string
end
create_table :admins do |t|
t.column :username, :string
t.column :pass_hash, :string
t.column :pass_salt, :string
t.column :token_auth, :string
end
end
init_default_configuration
Expand All @@ -53,25 +56,42 @@ def setup
def init_default_configuration
remove_session_classes
LetMeIn.configure do |c|
c.models = ['User']
c.attributes = ['email']
c.passwords = ['password_hash']
c.salts = ['password_salt']
c.models = ['User']
c.attributes = ['email']
c.passwords = ['password_hash']
c.salts = ['password_salt']
c.tokens = ['auth_token']
c.generate_tokens = [false]
end
LetMeIn.initialize
end

def init_custom_configuration
remove_session_classes
LetMeIn.configure do |c|
c.models = ['User', 'Admin']
c.attributes = ['email', 'username']
c.passwords = ['password_hash', 'pass_hash']
c.salts = ['password_salt', 'pass_salt']
c.models = ['User', 'Admin']
c.attributes = ['email', 'username']
c.passwords = ['password_hash', 'pass_hash']
c.salts = ['password_salt', 'pass_salt']
c.tokens = ['auth_token', 'token_auth']
c.generate_tokens = [false, false]
end
LetMeIn.initialize
end


def init_token_configuration
remove_session_classes
LetMeIn.configure do |c|
c.models = ['User', 'Admin']
c.attributes = ['email', 'username']
c.passwords = ['password_hash', 'pass_hash']
c.salts = ['password_salt', 'pass_salt']
c.tokens = ['auth_token', 'token_auth']
c.generate_tokens = [false, true]
end
LetMeIn.initialize
end

def remove_session_classes
Object.send(:remove_const, :UserSession) rescue nil
Object.send(:remove_const, :AdminSession) rescue nil
Expand All @@ -90,6 +110,7 @@ def test_default_configuration_initialization
assert_equal ['email'], LetMeIn.config.attributes
assert_equal ['password_hash'], LetMeIn.config.passwords
assert_equal ['password_salt'], LetMeIn.config.salts
assert_equal ['auth_token'], LetMeIn.config.tokens
end

def test_custom_configuration_initialization
Expand All @@ -98,11 +119,13 @@ def test_custom_configuration_initialization
c.attribute = 'username'
c.password = 'encrypted_pass'
c.salt = 'salt'
c.token = 'token_auth'
end
assert_equal ['Account'], LetMeIn.config.models
assert_equal ['username'], LetMeIn.config.attributes
assert_equal ['encrypted_pass'], LetMeIn.config.passwords
assert_equal ['salt'], LetMeIn.config.salts
assert_equal ['token_auth'], LetMeIn.config.tokens
end

def test_model_integration
Expand Down Expand Up @@ -223,4 +246,84 @@ def test_custom_admin_session
assert session.valid?
assert_equal admin, session.admin
end
end

def test_throw_error_for_model_not_found
begin
user = SubUser.create!(:email => '[email protected]', :password => 'pass')
rescue LetMeIn::Error => e
assert_equal 'SubUser must be added to your LetMeIn initializer', e.to_s
end
end

# Token Authentication Related

def test_generate_token_false_by_default
init_default_configuration
user = User.create!(:email => '[email protected]', :password => 'pass')
assert_equal [false], LetMeIn.config.generate_tokens
assert_nil user.auth_token
end

def test_generate_token
init_token_configuration
admin = Admin.create!(:username => 'admin', :password => 'pass')
assert_match /^.{40}$/, admin.token_auth
end

def test_token_session_init
init_token_configuration
session = AdminSession.new(:token_auth => "29dkd38kduhuf88wldke21")
assert_equal "29dkd38kduhuf88wldke21", session.token
assert_nil session.object
assert_nil session.admin
end

def test_ignore_token_session_init
init_default_configuration
session = UserSession.new(:auth_token => "29dkd38kduhuf88wldke21")
assert_nil session.token
assert_nil session.object
assert_nil session.user
end

def test_return_token_if_available
init_token_configuration
admin = Admin.create!(:username => 'admin', :password => 'pass')
session = AdminSession.create(:username => 'admin', :password => 'pass')
assert session.errors.blank?
assert_equal admin.token_auth, session.token
end

def test_token_authentication
init_token_configuration
admin = Admin.create!(:username => 'admin', :password => 'pass')
session = AdminSession.create(:token_auth => admin.token_auth)
assert session.errors.blank?
assert_equal admin, session.object
assert_equal admin, session.admin
end

def test_token_authentication_failure
init_token_configuration
admin = Admin.create!(:username => 'admin', :password => 'pass')
session = AdminSession.create(:token_auth => 'bad_token')
assert session.errors.present?
assert_equal 'Failed to authenticate', session.errors[:base].first
assert_nil session.object
assert_nil session.admin
end

def test_token_authentication_exception
init_token_configuration
admin = Admin.create!(:username => 'admin', :password => 'pass')
session = AdminSession.new(:token_auth => 'bad_token')
begin
session.save!
rescue LetMeIn::Error => e
assert_equal 'Failed to authenticate', e.to_s
end
assert_nil session.object
assert_nil session.admin
end

end