User authentication with Ramaze

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:

action="#{MainController.r(:login)}" method="POST">

Login form

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

/> />

for="password">Password
type="password" name="password" />

/> />

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

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:

action="#{MainController.r(:register)}" method="POST">

Register

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

/> />

for="password">Password
type="password" name="password" />

/> />

for="password_confirmation">Password confirmation
type="password" name="password_confirmation" />

/> />

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

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.