Justkez.com Ruby, Geospatial, Data-viz and life.


User authentication with Ramaze

Written on 16 Jul 2009 by Kester Dobson

This post outlines how to get database driven, session based user registration, authentication and login working with Ramaze and Sequel.

Introduction

Ramaze is, in my opinion, the leading Ruby web framework. Whilst the documentation and community examples are still gaining momentum, you can really benefit by spending some time learning the framework.

This post is a guide to getting user registration and authentication working. We are talking proper, database driven, session based authentication - not just a single secure login area.

Requirements

This guide is written based on Ramaze 2009.06 and Sequel 3.2.0 - earlier versions of both of these will work, but you will need the "Innated" version of Ramaze (something after 2009.05 would be best) and ideally Sequel 3.1.0. (Sequel is the database abstraction/ORM library, so you can be using SQLite, MySQL, Pg - whatever floats your database boat)

Getting started

We will be creating and editing several files - one Sequel-based database model, two controllers and a couple of view files. To save you some effort, I have provided a basic SQL schema for creating the user table.

create table users(
  id integer primary key,
  login varchar default null,
  email varchar default null,
  crypted_password varchar default null,
  salt text default null
  );

N.B. this is a SQLite formatted query.

We also need to make sure the Ramaze application is using Sequel, and we connect Sequel to the database with the above table. In this example, I have created tutorial.db3 with the above table.

In app.rb, add in the following lines:

require 'sequel'
# ...
DB = Sequel.connect('sqlite://tutorial.db3')

The controller

The controller, as you probably know, ties the interface (view) and database (model) together, we don't actually need to do much with regards to controller editing, as Ramaze has pretty sophisticated helpers for managing user logins.

With a new Ramaze project (ramaze --create my-project), open the controller/init.rb file and make it look like this:

class Controller < Ramaze::Controller
  layout :default
  helper :xhtml, :user
  engine :Etanni
end

private

 def login_first
   return if logged_in?
   redirect MainController.r(:login)
 end

require __DIR__('main')

Important things to note are the inclusion of the :user helper; you can read up on this built-in helper here.

As a final step, we need to add two methods to controller/main.rb - this will handle the login and registration process:

def login
  @title = "Login"
  redirect_referer if logged_in?
  return unless request.post?
  user_login(request.subset(:login, :password))
  redirect MainController.r(:index)
end

def register
  @title = "Register for an account"
  
  if request.post?
    @user = ::User.new
    @user[:email] = request[:email]
    @user.password = request[:password]
    @user.password_confirmation = request[:password_confirmation]
    @user.salt = Digest::SHA1.hexdigest("--#{Time.now.to_f}--#{user.email}--") 
    
    if @user.save
        flash[:message] = 'Account created, feel free to login below'
        redirect MainController.r(:index)
    end
  end
end

And whilst we're at it, we might as well add in the very, very basic logout action:

def logout
  flash[:message] = "Logged out"
  user_logout
end

The model

We need to define several model-level methods to help register, authenticate and login users. This will model will be a representation of the users table that you should have created in the first step.

Create a file, "user.rb", in model/, and you can make it look something like this:

class User < Sequel::Model(:users)  

  attr_accessor :password
  attr_accessor :password_confirmation

  def after_create
    self.crypted_password = encrypt(password)
    @new = false
    save
  end

  def authenticated?(password)
      crypted_password == encrypt(password)
  end

  def encrypt(password)
    self.class.encrypt(password, salt)
  end

  def self.encrypt(password, salt)
    Digest::SHA1.hexdigest("--#{salt}--#{password}--")
  end

  def self.authenticate(hash)
    email, pass = hash['login'], hash['password']

    if user = User[:email => email]
      return user unless pass
      user if user.authenticated?(pass)
    end
  end

end

Some interesting things to note...

  • We have user defined attributes for the password and password_confirmation fields - we do not store these in the database, but we need to assign and access them to verify the password and then hash it

  • The password is hashed by using SHA1, with a seed/key determined from the user password and another user-specific value stored as the salt. This means that each password is hashed differently, making things a bit more secure.

  • There is a bit of overlap between login and email here, as there will be in the following sections. I have chosen to demonstrate logging in with an email address, not a login name.

Don't forget...

You need to edit model/init.rb to require the model you just created.

The views

We will need to create two views, in addition to the default views that Ramaze creates for you. (Remember, a view is the webpage/interface that you actually see in the browser)

The login form

Create login.xhtml in view/, and use the following as a guide:

<form action="#{MainController.r(:login)}" method="POST">
  <fieldset>
    <legend>Login form</legend>

    <label for="login">E-mail address</label>
    <input type="text" name="login" />

    <br /><br />

    <label for="password">Password</label>
    <input type="password" name="password" />

    <br /><br />

    <input type="submit" value="Login" />

  </fieldset>
</form>

Believe it or not, we have nearly finished adding in user authentication; the final steps being the ability to register, and securing parts of your application.

The registration form

It is more than likely that you will want a slightly more exotic registration than the one below, but it provides a good starting point. This code should go in view/register.xhtml:

<form action="#{MainController.r(:register)}" method="POST">
  <fieldset>
    <legend>Register</legend>

    <label for="email">E-mail address</label>
    <input type="text" name="email" />

    <br /><br />

    <label for="password">Password</label>
    <input type="password" name="password" />

    <br /><br />

    <label for="password_confirmation">Password confirmation</label>
    <input type="password" name="password_confirmation" />

    <br /><br />

    <input type="submit" value="Register" />

  </fieldset>
</form>

Securing actions, accessing the logged in user

There are a couple of ways you can secure actions from within Ramaze - one of which you can see in the login method above. These techniques are:

def secret
  login_first
  # ... Secret stuff
end

Which will redirect the user to the login page. Alternatively, you can do a "manual" check to see if the user is logged in:

def secret
  redirect_referer unless logged_in?
  # .. or ..
  return if !logged_in?
  # .. or ..
  if logged_in?
    print 'You are logged in'
  end
end

Who is logged in?

It can be very useful to access information about the logged in user; if we use an example whereby a user wants to edit their comment on a blog...

def edit_comment id = nil
  login_first
  redirect_referer unless @comment = Comment[:id => id]
  redirect_referer unless @comment[:user_id] == user[:id]
end

This hypothetical snippet would ask the user to login first, then it would check to see if the comment exists. Finally it will check the user object to see if the ID lines up with the owner of the comment.

It is worth noting that user is accessible throughout almost all of the Ramaze application, so you can do checks within layouts, views, controllers (but not models).

Questions/problems?

Feel free to leave me a comment below with any issues you're having with the above, or if you have some improvements that can be made.

Comments/Discussion

blog comments powered by Disqus --