Rails JWT authentication
A JSON web token(JWT) is a JSON Object that is used to securely transfer information between two parties. JWT is widely used for securely authenticate and authorize user from the client in a REST API. In this post, I will go over step by step how to implement authentication using JWT in a rails API.
The gems we need:
gem 'bcrypt', '~> 3.1', '>= 3.1.12’
gem 'jwt', '~> 2.5’
gem 'rack-cors'
gem 'active_model_serializers', '~> 0.10.12’
After adding the gemfile run bundle install
Create the routes
post "/users", to: "users#create"
get "/me", to: "users#me"
post "/auth/login", to: "auth#login"
We will sign up new users making a POST request to /users. An existing user can log in by making a post request to “/auth/login” and a user can access user data by making a GET request to “/me”. We need 3 routes to the least, more routes can be added later as we go.
Add CORS
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
Cross-Origin Resource Sharing (CORS) is a middleware that will accept requests to the API from only one client URL. The client URL that we want to allow to make a request will go into the origins
. We set * origins, for now, that will allow anyone to make requests to our API for now.
Create the user model:
rails g model user username password_digest bio --no-test-framework
Add the has_secure_password
macro in the user model and validation for the username:
class User < ApplicationRecord
has_secure_password
validates :username, uniqueness: true
end
has_secure_password
is a bcrypt method that encrypts the password for each user. For this method to work we add password_digest
field to our database table. However, when we make a post request to our server we send password
. Bcrypt handles the rest for us.
Example request:
fetch('URL/auth/login',{
method: POST,
headers: {
'Content-type': 'application/json'
},
body: {
username: 'randomUserName',
password: 'ask^dsk34'
})
Adding JWT to our API
JSON Web Tokens are an open, industry-standard RFC 7519 method for representing claims securely between two parties. A JWT token looks like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Source - JWT.io
It has three parts. The first part is the header that contain the algorithm and token type. The second part is the payload, the data we want to store in the token. The third part is the signature, which contains the secret key. We are going to generate JWT tokens from the application_controller.rb
class ApplicationController < ActionController::API
def encode_token(payload)
JWT.encode(payload, 'hellomars1211')
end
def decoded_token
header = request.headers['Authorization']
if header
token = header.split(" ")[1]
begin
JWT.decode(token, 'hellomars1211')
rescue JWT::DecodeError
nil
end
end
end
end
The encode_token
method takes the payload as an argument. We will pass the user id as a payload. Then we call the JWT.encode(payload, 'hellomars1211')
method to encode our token. Our payload will be the user id which then we can use to find the correct user. Notice that we pass a string: 'hellomars1211'
, along with the payload as an argument, which will be our secret key, that we’ll also use to decode our token. A secret key can be any combination of chars, symbols, numbers, etc. We will call the decoded_token
method to decode a JWT token.
Whenever we make a request to a protected route or resource we pass the JWT token along with our data in the request, in the Authorization header using the Bearer schema. An example request:
fetch("URL/me", {
method: "GET",
headers: {
Authorization: `Bearer <token>`,
},
});
We access the token from the header and decode the token using JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')
method, we also need to pass the secret key in order to decode the token.
We can now access the user.id
from the decoded token. We will create a method current_user
that takes the user id from the decoded token and find the user using the same user id. This will give us the user that is currently logged in. We will create another method authorized
to check if we have a current_user that is logged in.
def current_user
if decoded_token
user_id = decoded_token[0]['user_id']
@user = User.find_by(id: user_id)
end
end
def authorized
unless !!current_user
render json: { message: 'Please log in' }, status: :unauthorized
end
end
Finally we will add a before_action
rule to the controller that will call the authorized
method before doing anything and check if the user is logged in. For any unauthorized request we will render a message: Please log in
. This is what our application_controller will look like:
*app/controllers/application_controller.rb*
class ApplicationController < ActionController::API
before_action :authorized
def encode_token(payload)
JWT.encode(payload, 'hellomars1211')
end
def decoded_token
header = request.headers['Authorization']
if header
token = header.split(" ")[1]
begin
JWT.decode(token, 'hellomars1211', true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
end
end
def current_user
if decoded_token
user_id = decoded_token[0]['user_id']
@user = User.find_by(id: user_id)
end
end
def authorized
unless !!current_user
render json: { message: 'Please log in' }, status: :unauthorized
end
end
end
Create the users_controller
rails g controller users
In the user controller, we will create an action for creating or signing up a new user. We will also create a token if a user signs up with valid data and send the token along with the response, this will make the user to be logged in right away when they sign up for our app. We will handle the sign up function in the create method inside the users_controller.rb.
class UsersController < ApplicationController
rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record
def create
user = User.create!(user_params)
@token = encode_token(user_id: user.id)
render json: {
user: UserSerializer.new(user),
token: @token
}, status: :created
end
private
def user_params
params.permit(:username, :password, :bio)
end
def handle_invalid_record(e)
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
end
We serialized our data to only return the user id, username, and bio. It wouldn’t make sense for us to return the password to the client and also the password is encrypted in our database.
class UserSerializer < ActiveModel::Serializer
attributes :id, :username, :bio
end
Now, remember when we added the before_action
rule to the application_controller? That will prevent us creating a new user if we are logged in. But that doesn’t make any sense. How can we log in when we didn’t even sign up yet or how can we log in when we can’t create a new user at all? Well, in order for us to bypass authorization we will add a skip_before_action
to the user controller and make exception for only the create
method. This will allow us to skip the authorization if we want to sign up a new user.
Also, we will create method me
to get the profile of the user that will return the current_user
that we set in the application_controller.
*app/controllers/users_controller.rb*
class UsersController < ApplicationController
skip_before_action :authorized, only: [:create]
rescue_from ActiveRecord::RecordInvalid, with: :handle_invalid_record
def create
user = User.create!(user_params)
@token = encode_token(user_id: user.id)
render json: {
user: UserSerializer.new(user),
token: @token
}, status: :created
end
def me
render json: current_user, status: :ok
end
private
def user_params
params.permit(:username, :password, :bio)
end
def handle_invalid_record(e)
render json: { errors: e.record.errors.full_messages }, status: :unprocessable_entity
end
end
Our sign-up is ready. Let’s try making some request it in the postman:
Let’s gooooooooooo!! Oh, wait! What else are we missing? The most important part of the auth: the LOGIN!
To implement login we will create a new controller and we will call it auth_controller.
rails g controller auth
*app/controllers/auth_controller.rb*
class AuthController < ApplicationController
skip_before_action :authorized, only: [:login]
rescue_from ActiveRecord::RecordNotFound, with: :handle_record_not_found
def login
@user = User.find_by!(username: login_params[:username])
if @user.authenticate(login_params[:password])
@token = encode_token(user_id: @user.id)
render json: {
user: UserSerializer.new(@user),
token: @token
}, status: :accepted
else
render json: {message: 'Incorrect password'}, status: :unauthorized
end
end
private
def login_params
params.permit(:username, :password)
end
def handle_record_not_found(e)
render json: { message: "User doesn't exist" }, status: :unauthorized
end
end
In the login
method we are first finding the user with the username, if the user is not found we return an error message: "User doesn't exist"
. After we find the user we authenticate the user with the password using bcrypt’s authenticate method. Once the authentication is complete we create a token for the user and return the user along with token. In case the authentication is failed we return an error message: 'Incorrect password'
. We also added the skip_before_action :authorized, only: [:login]
here just like for the create method in the users controller.
Let’s make some calls in postman:
Our auth is complete.
User authentication is 3 fold. At first we validate the data, then we authenticate the user with correct username, and password, and finally we authorize the user.
Validation—————>Authentication————>Authorization
We cannot build a logout function if we authenticate using JWT. The JWT library doesn’t come with a destroy method for the token. So, how can we log out? That has to be handled in the client. If we have a react client, we can store the token when we login in the localStorage and remove it from the localStorage if we want to log out.
fetch("http://localhost:3000/auth/login/", {
method: "POST",
headers: {
"Content-type": "application/json",
},
body: JSON.stringify({
username: username,
password: password,
}),
})
.then((res) => res.json())
.then((data) => {
localStorage.setItem("jwt", data.jwt);
})
And we simply remove the jwt token from the localStorage to logout:
localStorage.removeItem("jwt")
Top comments (2)
Such a detailed and informative blog, good job and thanks for your work.
Good stuff!
The quality of the post could be improved somewhat by formatting code examples to 2-space indent 🙈