Skip to content

Instantly share code, notes, and snippets.

@toretore
Created May 4, 2011 08:08
Show Gist options
  • Save toretore/954909 to your computer and use it in GitHub Desktop.
Save toretore/954909 to your computer and use it in GitHub Desktop.
Rails authentication
User authentication with Rails
This is all you need to have fully working and secure authentication in Rails.
Why use this instead of <bloated auth framework>? Well, there are a number of
reasons, but I guess you'll just have to go through a few apps where you
learn the hard way why the idea of a "fully featured" authentication
framework that doesn't get on your nerves is a mirage.
Don't simply copy and paste this without understanding everything it does. It's
a starting point and proof of concept more than anything else.
class ApplicationController < ActionController::Base
#The currently logged in user, if any
def current_user
#Use defined? to avoid additional finds when user is not found
@_current_user = session[:current_user_id] && User.find_by_id(session[:current_user_id]) unless defined?(@_current_user)
@_current_user
end
#Set the current user. Pass nil to unset, i.e. log out
def current_user=(user)
session[:current_user_id] = user && user.id
remove_instance_variable('@_current_user') if defined?(@_current_user) #Force reload in current_user
current_user
end
def logged_in?
!!current_user
end
def logged_out?
!logged_in?
end
def require_login
unless logged_in?
flash[:error] = 'You must be logged in to view this page'
redirect_to new_login_url
end
end
def require_logout
if logged_in?
flash[:error] = "You can't be logged in when viewing this page"
redirect_to login_url
end
end
end
class LoginsController < ApplicationController
skip_before_filter :require_login, :only => [:new, :create]
before_filter :require_logout, :only => [:new, :create]
def new
end
#POST /login?username=foo&password=bar
#Creates the login, AKA logs the user in
def create
if user = User.find_by_email(params[:email])
if user.password == params[:password]
self.current_user = user
redirect_to root_url
else
flash.now[:error] = 'Wrong password'
render 'new'
end
else
flash.now[:error] = "Could not find user with email \"#{params[:email]}\""
render 'new'
end
end
#DELETE /login
#Destroys the login, AKA logs the user out
def destroy
self.current_user = nil
redirect_to new_login_url
end
end
require 'test_helper'
class LoginsControllerTest < ActionController::TestCase
attr_reader :controller, :request, :response
setup do
@user = Factory.create(:user, :email => 'humbaba', :password => 'password')
end
test "should create login with matching username and password" do
post :create, :email => 'humbaba', :password => 'password'
assert controller.logged_in?
assert_equal @user, controller.current_user
end
test "should not create login with non-matching username and password" do
post :create, :email => 'humbaba', :password => 'assword'
assert !controller.logged_in?
assert_nil controller.current_user
post :create, :email => 'enkidu', :password => 'password'
assert !controller.logged_in?
assert_nil controller.current_user
end
test "should be logged in already with current_user_id in session" do
get :show, {}, {:current_user_id => @user.id}
assert controller.logged_in?
assert_equal @user, controller.current_user
end
test "should not break on non-existing user's id in session" do
get :show, {}, {:current_user_id => 12345}
assert !controller.logged_in?
assert_nil controller.current_user
end
test "should destroy login" do
delete :destroy, {}, {:current_user_id => @user.id}
assert !controller.logged_in?
assert_nil controller.session[:current_user_id]
end
end
YourApp::Application.routes.draw do
resource :login
end
require 'bcrypt'
class User < ActiveRecord::Base
def password
@password ||= password_hash && BCrypt::Password.new(password_hash)
end
def password=(password)
@password = nil #Force reload
self.password_hash = password && BCrypt::Password.create(password) #BCrypt will create a hash from nil
end
def self.authenticate(email, password)
if (user = find_by_email(email)) && user.password == password
user
else
nil
end
end
end
Factory.define :user do |u|
u.sequence(:email){|n| "email#{n}@example.com" }
u.password 'password'
end
require 'test_helper'
class UserTest < ActiveSupport::TestCase
setup do
@user = Factory.create(:user, :email => 'humb@ba', :password => 'password')
end
test "authenticate should return user when email and password match" do
assert_equal @user, User.authenticate('humb@ba', 'password')
end
test "authenticate should return nil when email and password do not match" do
assert_nil User.authenticate('humb@ba', 'assword')
assert_nil User.authenticate('ereshkigal', 'password')
end
test 'should be able to change password' do
assert @user.password == 'password' #assert_equal doesn't use ==, compare manually
@user.password = 'donkey'
assert @user.password == 'donkey'
@user.save!
assert User.find(@user.id).password == 'donkey'
assert User.authenticate('humb@ba', 'donkey')
end
test "should not treat nil as a password" do
@user.password = nil
assert_nil @user.password
assert_nil @user.password_hash
@user.password = 'notnil'
assert @user.password == 'notnil'
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment