Skip to content

Instantly share code, notes, and snippets.

@jnunemaker
Created July 15, 2009 03:33
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 27 You must be signed in to fork a gist
  • Save jnunemaker/147427 to your computer and use it in GitHub Desktop.
Save jnunemaker/147427 to your computer and use it in GitHub Desktop.
a starting point for authentication with mongomapper and rails
# include this in application controller
module Authentication
protected
# Inclusion hook to make #current_user and #signed_in?
# available as ActionView helper methods.
def self.included(base)
base.send :helper_method, :current_user, :signed_in?, :authorized? if base.respond_to? :helper_method
end
# Returns true or false if the user is signed in.
# Preloads @current_user with the user model if they're signed in.
def signed_in?
!!current_user
end
# Accesses the current user from the session.
# Future calls avoid the database because nil is not equal to false.
def current_user
@current_user ||= (sign_in_from_session || sign_in_from_basic_auth) unless @current_user == false
end
# Store the given user id in the session.
def current_user=(new_user)
session[:user_id] = new_user ? new_user.id : nil
@current_user = new_user || false
end
# Check if the user is authorized
#
# Override this method in your controllers if you want to restrict access
# to only a few actions or if you want to check if the user
# has the correct rights.
#
# Example:
#
# # only allow nonbobs
# def authorized?
# current_user.name != "bob"
# end
#
def authorized?(action=nil, resource=nil, *args)
signed_in?
end
# Filter method to enforce a sign_in requirement.
#
# To require sign_ins for all actions, use this in your controllers:
#
# before_filter :sign_in_required
#
# To require sign_ins for specific actions, use this in your controllers:
#
# before_filter :sign_in_required, :only => [ :edit, :update ]
#
# To skip this in a subclassed controller:
#
# skip_before_filter :sign_in_required
#
def authenticate
authorized? || access_denied
end
# Redirect as appropriate when an access request fails.
#
# The default action is to redirect to the sign_in screen.
#
# Override this method in your controllers if you want to have special
# behavior in case the user is not authorized
# to access the requested action. For example, a popup window might
# simply close itself.
def access_denied
respond_to do |format|
format.html do
store_location
redirect_to new_session_path
end
# format.any doesn't work in rails version < http://dev.rubyonrails.org/changeset/8987
# you may want to change format.any to e.g. format.any(:js, :xml)
format.any do
request_http_basic_authentication 'Web Password'
end
end
end
# Store the URI of the current request in the session.
#
# We can return to this location by calling #redirect_back_or_default.
def store_location
session[:return_to] = request.request_uri
end
# Redirect to the URI stored by the most recent store_location call or
# to the passed default. Set an appropriately modified
# after_filter :store_location, :only => [:index, :new, :show, :edit]
# for any controller you want to be bounce-backable.
def redirect_back_or_default(default)
redirect_to(session[:return_to] || default)
session[:return_to] = nil
end
# Called from #current_user. First attempt to sign_in by the user id stored in the session.
def sign_in_from_session
if session[:user_id]
self.current_user = User.find_by_id(session[:user_id])
end
end
# Called from #current_user. Now, attempt to sign_in by basic authentication information.
def sign_in_from_basic_auth
authenticate_with_http_basic do |email, password|
self.current_user = User.authenticate(email, password)
end
end
# This is ususally what you want; resetting the session willy-nilly wreaks
# havoc with forgery protection, and is only strictly necessary on sign_in.
# However, **all session state variables should be unset here**.
def sign_out_keeping_session!
# Kill server-side auth cookie
@current_user = false # not signed in, and don't do it for me
session[:user_id] = nil # keeps the session but kill our variable
# explicitly kill any other session variables you set
end
# The session should only be reset at the tail end of a form POST --
# otherwise the request forgery protection fails. It's only really necessary
# when you cross quarantine (signed-out to signed-in).
def sign_out_killing_session!
sign_out_keeping_session!
reset_session
end
end
# factory girl factories
Factory.define(:user) do |u|
u.sequence(:email) { |n| "john#{n}@doe.com" }
u.password "testing"
u.password_confirmation "testing"
end
<%- form_for @user do |form| -%>
<%= form.error_messages %>
<ul>
<li>
<%= form.label :email %>
<%= form.text_field :email %>
</li>
<li>
<%= form.label :password %>
<%= form.password_field :password %>
</li>
<li>
<%= form.label :password_confirmation, 'Confirm Password' %>
<%= form.password_field :password_confirmation %>
</li>
<li class="submit">
<%= submit_tag 'Sign Up', :disable_with => 'Signing you up...' %>
or <%= link_to 'sign in', new_session_path %>
</li>
</ul>
<%- end -%>
<%- form_tag session_path do -%>
<ul>
<li>
<%= label_tag 'email', 'Email' %>
<%= text_field_tag :email %>
</li>
<li>
<%= label_tag 'password', 'Password' %>
<%= password_field_tag :password %>
</li>
<li class="submit">
<%= submit_tag 'Sign In', :disable_with => 'Signing you in...' %>
or <%= link_to 'sign up', new_user_path %>
</li>
</ul>
<%- end -%>
class UsersController < ApplicationController
def new
@user = User.new
render :layout => 'sessions'
end
def create
@user = User.new(params[:user])
if @user.save
self.current_user = @user
redirect_to root_path
else
render 'new', :layout => 'sessions'
end
end
end
class ActiveSupport::TestCase
# clear each collection in setup
def setup
Dir[Rails.root + 'app/models/**/*.rb'].each do |model_path|
model_name = File.basename(model_path).gsub(/\.rb$/, '')
klass = model_name.classify.constantize
klass.collection.clear
end
end
# stuff removed for brevity
end
# uses bcrypt-ruby gem for password hashing be sure to config.gem it
require 'digest/sha1'
class User
include MongoMapper::Document
key :email, String, :required => true
key :crypted_password, String
key :reset_password_code, String
key :reset_password_code_until, Time
RegEmailName = '[\w\.%\+\-]+'
RegDomainHead = '(?:[A-Z0-9\-]+\.)+'
RegDomainTLD = '(?:[A-Z]{2}|com|org|net|gov|mil|biz|info|mobi|name|aero|jobs|museum)'
RegEmailOk = /\A#{RegEmailName}@#{RegDomainHead}#{RegDomainTLD}\z/i
def self.authenticate(email, secret)
u = User.first(:conditions => {:email => email.downcase})
u && u.authenticated?(secret) ? u : nil
end
validates_length_of :email, :within => 6..100, :allow_blank => true
validates_format_of :email, :with => RegEmailOk, :allow_blank => true
PasswordRequired = Proc.new { |u| u.password_required? }
validates_presence_of :password, :if => PasswordRequired
validates_confirmation_of :password, :if => PasswordRequired, :allow_nil => true
validates_length_of :password, :minimum => 6, :if => PasswordRequired, :allow_nil => true
def authenticated?(secret)
password == secret ? true : false
end
def password
if crypted_password.present?
@password ||= BCrypt::Password.new(crypted_password)
else
nil
end
end
def password=(value)
if value.present?
@password = value
self.crypted_password = BCrypt::Password.create(value)
end
end
def email=(new_email)
new_email.downcase! unless new_email.nil?
write_attribute(:email, new_email)
end
def password_required?
crypted_password.blank? || !password.blank?
end
def set_password_code!
seed = "#{email}#{Time.now.to_s.split(//).sort_by {rand}.join}"
self.reset_password_code_until = 1.day.from_now
self.reset_password_code = Digest::SHA1.hexdigest(seed)
save!
end
end
require 'test_helper'
class SessionsControllerTest < ActionController::TestCase
context "Visiting the sign in form" do
setup { get :new }
should_respond_with :success
should "display signup form" do
assert_select 'form[action=?]', session_path do
assert_select 'input[id=?]', 'email'
assert_select 'input[id=?]', 'password'
end
end
end
context "Submitting the sign in form with good credentials" do
setup do
@user = Factory(:user)
post :create, :email => @user.email, :password => 'testing'
end
should_be_signed_in_as { @user }
should_redirect_to_root
end
context "Submitting the sign in form with bad credentials" do
setup do
@user = Factory(:user)
post :create, :email => @user.email, :password => 'FAIL'
end
should_not_be_signed_in
should_redirect_to('sign in') { new_session_path }
end
context "Logging out" do
setup do
sign_in_as 'john@doe.com'
delete :destroy
end
should_not_be_signed_in
should_respond_with :success
end
end
require 'test_helper'
class UserTest < ActiveSupport::TestCase
should "validate presence of email" do
user = User.new
user.should have_error_on(:email)
user.email = 'foo@bar.com'
user.should_not have_error_on(:email)
end
should "validate length of email" do
user = User.new
user.email = 'a@a'
user.should have_error_on(:email)
user.email = 'r@a.wk'
end
should "always store email as lower case" do
user = User.new
user.email = 'F@FOOBAR.COM'
user.email.should == 'f@foobar.com'
end
should "be able to set user's reset password code" do
user = Factory(:user)
user.reset_password_code.should be_nil
user.reset_password_code_until.should be_nil
user.set_password_code!
user.reset_password_code.should_not be_nil
user.reset_password_code_until.should_not be_nil
end
context "Authentication" do
should 'work with existing email and correct password' do
user = Factory(:user)
User.authenticate(user.email, 'testing').should == user
end
should 'work with existing email (case insensitive) and password' do
user = Factory(:user)
User.authenticate(user.email.upcase, 'testing').should == user
end
should 'not work with existing email and incorrect password' do
User.authenticate('john@doe.com', 'foobar').should be_nil
end
should 'not work with non-existant email' do
User.authenticate('foo@bar.com', 'foobar').should be_nil
end
end
context "password" do
should 'be required if crypted password is blank' do
User.new.should have_error_on(:password)
end
should 'not be required if crypted password is present' do
user = User.new
user.crypted_password = BCrypt::Password.create('foobar')
user.should_not have_error_on(:password)
end
should "validate the length of password" do
user = User.new
user.password = '1234'
user.should have_error_on(:password)
user.password = '123456'
user.should_not have_error_on(:password)
end
end
end
require 'test_helper'
class UsersControllerTest < ActionController::TestCase
context "Visiting the signup form" do
setup { get :new }
should_respond_with :success
end
context "Attempting to signup with valid information" do
setup { post :create, :user => Factory.attributes_for(:user) }
should_change 'User.count', :by => 1
should "sign user in" do
session[:user_id].should == assigns(:user).id
end
should_redirect_to('home page') { root_path }
end
context "Attempting to signup with invalid information" do
setup { post :create, :user => {}}
should_not_change 'User.count'
should_not_be_signed_in
should_render_template :new
end
end
@michalvalasek
Copy link

Hey @jnunemaker, thanks for sharing the code! There is one mistake tho: UsersController is defined in file called sessions_controller.rb and there doesn't seem to be file with SessionsController. Also, SessionsControllerTest is defined in file called user_controller_test.rb.

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