Artur Chmaro

blog about web technologies

Rails5 - simple authentication endpoint

Ruby on Rails have some really nice gems (devise) to handle authentication, but I've decided to develop something from scratch. Maybe it would be useful for some Rails cadets.

When it comes to developing authentication system it is very important to keep our user's passwords safe. That means we cannot store them in plain text. Luckily, Ruby on Rails 5 has ActiveModel::SecurePassword on board, so all passwords will be saved as secure digests in our database.

First, we need a User model:

$ rails g model User full_name:string email:string password_digest:string

Once model is generated we need to run database migrations:

$ bundle exec rake:db:migrate

Add has_secure_password attribute to User model:

# app/models/user.rb
class User < ApplicationRecord
  has_secure_password
end

Add required bcrypt gem to Gemfile:

gem 'bcrypt'

Install gem:

$ bundle

Once is installation finished you are free to go to your Rails console where you can create some test User and play a bit with authentication:

User.create(full_name: 'Artur Ch', email: 'chmarus@gmail.com', password: 'secret666', password_confirmation: 'secret666')

User.find_by(email: 'chmarus@gmail.com').authenticate('asd') => false
User.find_by(email: 'chmarus@gmail.com').authenticate('secret666') => User

Authentication works, so we can build our first endpoint. I want to obtain User entity by sending POST request to /api/v1/user_sessions?user[email]=email@test.com&user[password]=password

Here is necessary route:

namespace :api do
  namespace :v1 do
    resources :user_sessions, only: [:create]
  end
end

Route is useless without a controller, so we need it as well:

# app/controllers/api/v1/user_sesssions_controller.rb
class Api::V1::UserSessionsController < Api::V1::BaseController
end

# app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ApplicationController
end

Now it's a tricky part because we need to validate user's email and password params. When everything is fine I want to return JSON object with the full name, email and id. If provided credentials are wrong (there is no user with given e-mail or/and password is not valid) JSON object should contain an error message. The controllers are not responsible for such logic, so let's write some tiny class that will do the job:

# app/services/authenticate_user.rb
class AuthenticateUser
  class UserNotFound < StandardError; end

  class << self
    def call(email, password)
      user = User.find_by(email: email).try(:authenticate, password)
      fail UserNotFound unless user

      user
    end
  end
end

Rails5 is using Spring so we need to edit its config to include services directory:

# config/spring.rb
%w(
  .ruby-version
  .rbenv-vars
  tmp/restart.txt
  tmp/caching-dev.txt
  app/services
).each { |path| Spring.watch(path) }

We are almost done, but we need to think about the way how we want to serialize User model into JSON object. There are plenty of ways to do it, but I've decided to use activemodelserializers. To use this gem just install it:

gem 'active_model_serializers', '~> 0.10.0'

Create serializer for User model:

# app/serializers/user_serializer.rb
class UserSerializer < ActiveModel::Serializer
  attributes :id, :full_name, :email
end

Now we can modify controller so it will use our authentication service:

class Api::V1::UserSessionsController < Api::V1::BaseController
  def create
    @user = AuthenticateUser.call(email, password)
    render json: @user
  end

  private

  def user_params
    params.require(:user).permit(:email, :password)
  end

  def email
    user_params[:email]
  end

  def password
    user_params[:password]
  end
end

When you run your development server and perform test request with proper e-mail and password params everything should work fine. Problem is that our app will crash when someone will send a wrong password or no params at all. We can solve it by catching those errors gently in our base controller:

class Api::V1::BaseController < ApplicationController
  protect_from_forgery prepend: true

  rescue_from ActionController::ParameterMissing do |e|
    render json: { error: e.message }, status: :unprocessable_entity
  end

  rescue_from AuthenticateUser::UserNotFound do |e|
    render json: { error: 'User not found.' }, status: :not_found
  end

end

Now we can test endpoint:

Looks like our simple authentication endpoint is working. The full code you can find [here](https://github.com/Chmarusso/web_chatter/pull/1/files). In next post, I will show you how to write tests for it.