Photo by Markus Spiske on Unsplash

Add authentication to your Rails API application

Elson Akio Otake
5 min readFeb 10, 2023

--

Video showing the steps involved in creating API authentications

I recently needed to create API documentation that tested endpoints. The difficulties I had in finding information on how to do this motivated me to share this knowledge through these articles.
In my previous article titled Documenting and Testing Your API, I covered documenting the REST API by doing CRUD in a Rails application. In this new article, we will implement user authentication. In my next article, I will continue the documentation created in the first one, including the authentication. So, if you haven’t already, I suggest following the steps in the previous article or cloning the article’s repository.

Let’s install the JWT and bcrypt gems and set up authentication on controllers.

Installation of JWT and bcrypt gems

We will install the JWT and bcrypt gems. JSON Web Tokens (JWT) are an open, industry-standard RFC 7519 method for representing claims securely between two parties, and bcrypt is a password-hashing function based on the Blowfish cipher.

Add gems to your Gemfile.

# Use JSON Web Token (JWT) for token-based authentication
gem 'jwt'

# Use ActiveModel has_secure_password
gem 'bcrypt'

Run bundle.

bundle install

To use the password field as data entry, we need to create a variable with the name followed by _digest. Bcrypt will accept password input, encode that information and store it in the password_digest field. We don’t need to create a password column. Add the password_digest column to the users’ table.

rails generate migration AddPasswordDigestToUsers password_digest

Run the migration.

rake db:migrate

Add bcrypt’s has_secure_password on validation on app/models/user.rb.

class User < ApplicationRecord
has_secure_password
validates :email, presence: true, uniqueness: true
end

Add password to the list of allowed parameters of the user controller’s json_params method on app/controllers/api/v1/users_controller.rb.

# Only allow a list of trusted JSON parameters through.
def json_params
allowed_data = %(email password description).freeze
json_payload.select { |allow| allowed_data.include?(allow) }
end

Setup authentication on controllers

Create JsonWebToken class on app/models/json_web_token.rb.

class JsonWebToken
SECRET_KEY = Rails.application.secrets.secret_key_base.to_s

def self.encode(payload, exp = 24)
payload[:exp] = exp.hours.from_now.to_i
JWT.encode(payload, SECRET_KEY)
end

def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY)[0]
HashWithIndifferentAccess.new decoded
end
end

Rails generate a secret key stored in the SECRET_KEY variable. JWT uses this key to encode and decode contents.

In our example, the encode function receives a user_id and an expiration date. JWT creates an encoded token with this content.

The decode function takes an encoded token and decodes its contents using JWT and the key. It throws an error if the token is expired.

In app/controllers/application_controller.rb create the authorize_request function.

class ApplicationController < ActionController::API
def json_payload
return [] if request.raw_post.empty?

HashWithIndifferentAccess.new(JSON.parse(request.raw_post))
end

def authorize_request
header = request.headers['Authorization']
if header
header = header.split.last
begin
@decoded = JsonWebToken.decode(header)
@current_user = User.find_by_id!(@decoded[:user_id])
rescue ActiveRecord::RecordNotFound || JWT::DecodeError => e
render json: { errors: e.message }, status: :unauthorized
end
else
render json: { errors: 'Unauthorized user' }, status: :unauthorized
end
end
end

Let’s authenticate the user using a header. The header must provide the value of the token in the Authorization key. The authorize_request function checks if the supplied token is regular and has an existing user.

Enforce authorize_request for some user actions in the controller.

class Api::V1::UsersController < ApplicationController
before_action :authorize_request, only: %i[ show update destroy ]

Now we need to create the user login function. Create the authentication controller.

rails generate controller authentication --no-request-specs

On app/controllers/authentication_controller.rb add the following content to the controller.

class AuthenticationController < ApplicationController

# POST /auth/login
def login
allowed_data = %(email password).freeze
expiration = 24
data = json_payload.select { |allow| allowed_data.include?(allow) }
@user = User.find_by_email!(data[:email])
if @user.authenticate(data[:password])
token = JsonWebToken.encode(user_id: @user.id, exp: expiration)
time = Time.now + expiration.hours.to_i
render json: {
token:,
exp: time.strftime('%m-%d-%Y %H:%M'),
email: @user.email,
id: @user.id
},
status: :ok
else
render json: { error: 'Unauthorized' }, status: :unauthorized
end
rescue ActiveRecord::RecordNotFound
render json: { errors: 'Email not found' }, status: :not_found
end
end

The login function takes the email and password as body parameters. Check if the email exists. It uses bcrypt’s authenticate method to validate the user’s password. After that, generates the token with the user_id and expiration date.

Create the login route in config/routes.rb. Let’s link the auth/login path with the authentication controller’s login action.

post 'api/v1/auth/login', to: 'authentication#login'

Use of authentication

We can now test the authentication. Start or restart the server, as we have created a new route.

rails server
CTRL+C
rails server

In my next article, I intend to show how to update the documentation for cases involving authentications. For now, let’s test authentication using Postman.

Let’s create a user with its corresponding password. To list users and to create a new user are not in the list of actions protected by the authorize_request above. Make a POST request at the address http://localhost:3000/api/v1/users informing the parameters in the body.

Create a new user

Let’s log in as this new user. It’s a POST request to the address http://localhost:3000/api/v1/auth/login passing the parameters in the body.

User log in

A successful login returns with the information defined in the login method (AuthenticationController). In our case, token, expiration date, email, and user id.

Let’s now test one of the actions protected by the authorize_request method. We will update the user with id 2, including a description. We will make a PUT request to the address http://localhost:3000/api/v1/users/2, informing the new data in the body field.

Unauthorized operation

The operation was not successful because our authentication data was missing. The token returned in the login operation must be informed in the Authorization parameter of the Header field. Repeat this procedure for all actions that require authentication.

Authorized request

You can now test the show and delete protected actions.

If you consider the information in this article valuable, please share it.

The source data for this example is available in my documentation repository.

The previously created API documentation no longer works for show, update, and delete, as authentication is now required. We’ll fix that in my next article when I show you how to include authentication in the API documentation.

--

--