An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
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
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>`, },});
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:
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:
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
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To