Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
November 4, 2022 03:42 pm GMT

A Complete Guide to Rails Authentication Using JWT

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.12gem 'jwt', '~> 2.5gem '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]  endend

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: trueend

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-standardRFC 7519 method for representing claims securely between two parties. A JWT token looks like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Image description

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    endend

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 well 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>`,  },});

Image description

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)    endenddef authorized    unless !!current_user    render json: { message: 'Please log in' }, status: :unauthorized    endend

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    endend

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    endend

We serialized our data to only return the user id, username, and bio. It wouldnt 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, :bioend

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 doesnt make any sense. How can we log in when we didnt even sign up yet or how can we log in when we cant 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    endend

Our sign-up is ready. Lets try making some request it in the postman:

Image description

Lets 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    endend

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 bcrypts 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.

Lets make some calls in postman:

Image description

Image description

Image description

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 doesnt 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")

Original Link: https://dev.to/mohhossain/a-complete-guide-to-rails-authentication-using-jwt-403p

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To