How To Implement Simple Authentication Without Devise
Almost every web app requires a sign in form. In Ruby, the popular choice is the Devise gem for Rails.
Devise gives you a lot of functionality out of the box, but it can also be frustrating. It has a lot of “magic,” and is heavily coupled to various parts of Rails. If you’re requirements don’t exactly fit the gem – for example, anonymous users – you will be wading through documentation, how-to’s, and the source code of a few gems. That’s no fun, and it might be more trouble than it’s worth. And when that auth-related change request appears in a few months, will it be easy, or will you be facing a maintenance nightmare?
Authentication is actually pretty simple! If you have minimal requirements, you can roll your own authentication with less than 100 lines of code. In this article, I’m going to show you a tiny web app featuring sign in, sign out, and new user creation, with a sweet and simple manual implementation.
All the source code is available as a single-file here: simple_authentication.rb
Dependencies
The only real dependency is the BCrypt gem, which is used to hash the passwords. This is the gem that Devise and Rails already use by default – we will just be using it directly.
We will also be using Sinatra, but the code is very easy to translate to Rails. That’s because BCrypt is nicely decoupled from everything, unlike Devise.
Here is the Gemfile:
source 'https://rubygems.org'
gem 'sinatra', '~> 1.4'
gem 'bcrypt', '~> 3.1'
Hashing Passwords With BCrypt
The implementation revolves around two one-line methods:
def hash_password(password)
BCrypt::Password.create(password).to_s
end
def test_password(password, hash)
BCrypt::Password.new(hash) == password
end
The hash_password
function takes a plain text password and encrypts it with a strong one-way algorithm.
The idea is that it is impossible, or at least very difficult, to work out what the password was based on the hash.
We never store the password, only the hash, so that the original password is protected in the event that someone breaks into our database.
The second function test_password
is used to check whether a given password is the same as the one we have hashed earlier.
We don’t have the original password anymore, so this works by hashing the given password and comparing both the hashes.
This is basic password security, and the bcrypt gem handles all the details for us.
The User Model
To keep this example ultra simple, the User
model is just a plain old Struct
, and the “database” is just an array.
User = Struct.new(:id, :username, :password_hash)
USERS = [
User.new(1, 'bob', hash_password('the builder')),
User.new(2, 'sally', hash_password('go round the sun')),
]
Each user has just three properties: an id, a username, and the hash of their password.
If this was Rails, you would just create a User
model with those three attributes.
Signing In
This is the code that runs when the user submits the sign in form:
post '/sign_in' do
user = USERS.find { |u| u.username == params[:username] }
if user && test_password(params[:password], user.password_hash)
session.clear
session[:user_id] = user.id
redirect '/'
else
@error = 'Username or password was incorrect'
erb :sign_in
end
end
Security Note: On successful sign in, we clear the session before storing the user id. This is a security measure to prevent Session Fixation Attacks.
First we try and find the user with the given username.
If that user exists and test_password
says that the password is correct, then we have a successful sign in.
When successful, we store the user’s id in the session and redirect to the home page.
Otherwise, we just set an error message and render the sign in form again.
Accessing The Current User
Once the user is signed in, we need to know who they are on all future requests to app. Here is the code that returns the current user:
def current_user
if session[:user_id]
USERS.find { |u| u.id == session[:user_id] }
else
nil
end
end
We check the session to see if it contains the user id that we set during sign in.
If the user id exists, we find the user with that id.
If this was Rails, you would do something like User.find(session[:user_id])
.
You can also memoize the return value so that the current_user
method doesn’t hit the database every time it is called.
In the template for the home page, we use current_user
to say hello:
<p>Hello, <%= current_user.username %>.</p>
Signing Out
This is the code that runs when the user clicks the sign out button:
post '/sign_out' do
session.clear
redirect '/sign_in'
end
Since the user’s id is stored in the session, we can just clear the session to sign them out. Nice and simple!
Creating A New User
This is the code that runs after submitting the “Create New User” form:
post '/create_user' do
USERS << User.new(
USERS.size + 1,
params[:username],
hash_password(params[:password])
)
redirect '/'
end
In this little example app, we just create a new User
object and append it to the array of all users.
It would be the same in Rails except, instead of appending to an array, you would insert the new User
object into the database.
Notice how the hash_password
method is used.
Remember that we never store the plain text password, only it’s hash.
The End
There you have it: a simple, working implementation of basic authentication. That wasn’t too hard, was it?
Try it out for yourself. The whole app is available as a single file here: simple_authentication.rb
Got questions? Comments? Milk?
Shoot an email to [email protected] or hit me up on Twitter (@tom_dalling).