JSON Web Tokens (JWT) have become the go-to solution for stateless authentication in modern web applications, especially when building APIs. If you’re working with Ruby on Rails and want to implement JWT authentication, you’re in for a treat. Let’s dive into how you can set this up in your Rails API.
First things first, you’ll need to add the necessary gems to your Gemfile. The ‘jwt’ gem is essential for handling JWT operations, and ‘bcrypt’ is crucial for securely hashing passwords. Add these lines to your Gemfile:
gem 'jwt'
gem 'bcrypt'
Don’t forget to run bundle install
after adding these gems.
Now, let’s create a simple User model to work with. Run the following command in your terminal:
rails generate model User email:string password_digest:string
This will create a User model with email and password_digest fields. The password_digest field will store the hashed version of the user’s password.
Next, we need to set up our User model to use bcrypt for password hashing. Open up app/models/user.rb and add the following line:
class User < ApplicationRecord
has_secure_password
end
The has_secure_password
method is provided by ActiveModel and adds methods to set and authenticate against a BCrypt password.
Now, let’s create a JWT service to handle token creation and decoding. Create a new file at app/services/json_web_token.rb and add the following code:
class JsonWebToken
SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
def self.encode(payload, exp = 24.hours.from_now)
payload[:exp] = exp.to_i
JWT.encode(payload, SECRET_KEY)
end
def self.decode(token)
decoded = JWT.decode(token, SECRET_KEY)[0]
HashWithIndifferentAccess.new decoded
end
end
This service provides two class methods: encode
for creating JWTs and decode
for verifying and decoding JWTs. The SECRET_KEY is used to sign the tokens, ensuring they can’t be tampered with.
Now, let’s create an AuthenticationController to handle user login. Run the following command:
rails generate controller Authentication
Open the newly created app/controllers/authentication_controller.rb and add the following code:
class AuthenticationController < ApplicationController
def login
@user = User.find_by(email: params[:email])
if @user&.authenticate(params[:password])
token = JsonWebToken.encode(user_id: @user.id)
render json: { token: token }, status: :ok
else
render json: { error: 'unauthorized' }, status: :unauthorized
end
end
end
This controller action finds a user by email, authenticates the password, and if successful, creates a JWT containing the user’s ID.
We also need to create a mechanism to authenticate requests using the JWT. Let’s create an AuthorizeApiRequest service. Create a new file at app/services/authorize_api_request.rb and add the following code:
class AuthorizeApiRequest
def initialize(headers = {})
@headers = headers
end
def call
{
user: user
}
end
private
attr_reader :headers
def user
@user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
rescue ActiveRecord::RecordNotFound
nil
end
def decoded_auth_token
@decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
end
nil
end
end
This service extracts the token from the Authorization header, decodes it, and returns the corresponding user.
Now, let’s update our ApplicationController to use this service for authenticating requests:
class ApplicationController < ActionController::API
def authenticate_request
@current_user = AuthorizeApiRequest.new(request.headers).call[:user]
render json: { error: 'Not Authorized' }, status: 401 unless @current_user
end
end
You can now use this authenticate_request
method as a before_action in any controller where you want to require authentication.
Let’s create a simple API endpoint to test our JWT authentication. Create a new controller:
rails generate controller Api::V1::Posts
Open app/controllers/api/v1/posts_controller.rb and add the following code:
module Api
module V1
class PostsController < ApplicationController
before_action :authenticate_request
def index
posts = Post.all
render json: { status: 'SUCCESS', message: 'Loaded posts', data: posts }, status: :ok
end
end
end
end
Don’t forget to set up your routes in config/routes.rb:
Rails.application.routes.draw do
post 'login', to: 'authentication#login'
namespace :api do
namespace :v1 do
resources :posts, only: [:index]
end
end
end
Now, when a user wants to access the posts, they first need to log in to get a token. They can then use this token in subsequent requests to access protected resources.
Here’s how you might use this in practice:
- First, a user would log in:
POST /login
{
"email": "[email protected]",
"password": "password123"
}
If successful, this would return a token:
{
"token": "eyJhbGciOiJIUzI1NiJ9..."
}
- Then, to access protected resources, include this token in the Authorization header:
GET /api/v1/posts
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
This setup provides a solid foundation for JWT authentication in your Rails API. However, there are a few more things to consider for a production-ready system.
First, you might want to add token expiration. You can do this by including an expiration time in the JWT payload and checking it in the AuthorizeApiRequest service.
Second, consider implementing refresh tokens. These are long-lived tokens that can be used to obtain new access tokens without requiring the user to log in again.
Third, think about token revocation. Since JWTs are stateless, you can’t simply delete them server-side like you would with session-based authentication. One approach is to maintain a blacklist of revoked tokens.
Fourth, always use HTTPS in production to protect the tokens in transit.
Lastly, remember that while JWTs are great for stateless authentication, they’re not suitable for storing sensitive information. The payload of a JWT is encoded, not encrypted, which means anyone can read its contents if they have the token.
Implementing JWT authentication in your Rails API opens up a world of possibilities. It allows you to build stateless, scalable applications that can easily integrate with various front-end frameworks or mobile apps. Plus, it’s a great skill to have in your toolbox as a Rails developer.
As you work with JWTs, you’ll likely encounter various challenges and edge cases. Don’t get discouraged! Each problem you solve will deepen your understanding and make you a better developer. Remember, the Rails community is vast and supportive, so don’t hesitate to reach out for help when you need it.
Happy coding, and may your tokens always be valid!