Introduction
When building a web application with separate frontend and backend, securing the API is a crucial aspect. In this blog post, we will discuss how to secure a Ruby on Rails API using authentication with JSON Web Tokens (JWT) and how to consume it in a React application.
Assumptions
This blog post assumes that the reader is familiar with Ruby on Rails and React. If you are not familiar with these technologies, you can find plenty of resources online to learn the basics. Additionally, we assume that you already have a working Rails API and a React application that needs to consume it.
Setting up the Rails API
First, let's start by setting up the Rails API. We will use the devise-jwt
gem to handle authentication with JWT. Add the following lines to your Gemfile:
gem 'devise'
gem 'devise-jwt'
Then, run bundle install
to install the gems. Next, run the Devise generator to create a User model with some necessary fields:
rails g devise:install
rails g devise User
rails db:migrate
Now, we need to configure the devise-jwt
gem. Create a new file config/initializers/devise.rb
and add the following lines:
Devise.setup do |config|
# ...
config.jwt do |jwt|
jwt.secret = ENV['DEVISE_JWT_SECRET_KEY']
jwt.expiration_time = 1.day.to_i
jwt.dispatch_requests = [
['POST', %r{^/users/sign_in$}]
]
end
end
This configuration sets the JWT secret key, the expiration time for the token, and the endpoint for the sign-in action.
Next, let's create a sessions_controller.rb
file to handle the sign-in action:
class SessionsController < Devise::SessionsController
respond_to :json
def create
user = User.find_for_authentication(email: params[:user][:email])
if user&.valid_password?(params[:user][:password])
token = JWT.encode({ user_id: user.id }, ENV['DEVISE_JWT_SECRET_KEY'])
response.set_cookie('jwt', { value: token, httponly: true, secure: Rails.env.production? })
render json: { success: true, user: { email: user.email } }
else
render json: { success: false, errors: ['Invalid email or password'] }
end
end
end
This controller action finds the user by email and validates the password. If the credentials are correct, it generates a JWT and sets it as a cookie in the response.
Now, let's create a posts_controller.rb
file to handle the API requests:
class PostsController < ApplicationController
before_action :authenticate_user!
def index
render json: { posts: Post.all }
end
end
This controller action uses the authenticate_user!
method to ensure that only authenticated users can access it.
Finally, let's create a routes.rb
file to define the API routes:
Rails.application.routes.draw do
devise_for :users, controllers: { sessions: 'sessions' }
resources :posts, only: [:index]
end
This configuration sets up the Devise routes for user authentication and the API route for the posts#index
action.
Setting up the React application
Now that we have the Rails API set up, let's move on to the React application. We will use the axios
library to make API requests and the js-cookie
library to handle cookies.
First, let's install the axios
library:
npm install axios
Then, let's create a api.js
file to define the API endpoints:
import axios from "axios";
const API_BASE_URL = "http://localhost:3000";
export const signIn = async (email, password) => {
try {
const response = await axios.post(`${API_BASE_URL}/users/sign_in`, {
user: { email, password },
});
return response.data;
} catch (error) {
throw error.response.data.errors[0];
}
};
export const getPosts = async jwt => {
try {
const response = await axios.get(`${API_BASE_URL}/posts`, {
headers: { Authorization: `Bearer ${jwt}` },
});
return response.data.posts;
} catch (error) {
throw error;
}
};
This file defines two functions: signIn
to handle the sign-in action and getPosts
to retrieve the posts from the API. The Authorization
header is set with the JWT token.
Next, let's create a AuthContext.js
file to manage the user authentication state:
import React, { createContext, useState } from "react";
import { signIn as apiSignIn } from "./api";
import Cookies from "js-cookie";
export const AuthContext = createContext();
const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const signIn = async (email, password) => {
const { success, user } = await apiSignIn(email, password);
if (success) {
Cookies.set("jwt", user.token, { expires: 1, secure: true });
setUser({ email: user.email });
}
};
const signOut = () => {
Cookies.remove("jwt");
setUser(null);
};
return (
<AuthContext.Provider value={{ user, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
This file defines the AuthContext
and the AuthProvider
components to manage the user authentication state. The signIn
function sends the email and password to the API and sets the JWT as a cookie in the browser. The signOut
function removes the cookie and sets the user state to null
.
Finally, let's create a Posts.js
file to display the posts:
import React, { useContext, useEffect, useState } from "react";
import { AuthContext } from "./AuthContext";
import { getPosts as apiGetPosts } from "./api";
const Posts = () => {
const { user } = useContext(AuthContext);
const [posts, setPosts] = useState([]);
useEffect(() => {
const getPosts = async () => {
try {
const jwt = Cookies.get("jwt");
const posts = await apiGetPosts(jwt);
setPosts(posts);
} catch (error) {
console.error(error);
}
};
if (user) {
getPosts();
}
}, [user]);
return (
<div>
{posts.map(post => (
<div key={post.id}>
<h3>{post.title}</h3>
<p>{post.content}</p>
</div>
))}
</div>
);
};
export default Posts;
This file defines the Posts
component to display the posts retrieved from the API. The getPosts
function retrieves the JWT from the cookie and sends it with the API request.
Conclusion
In this blog post, we discussed how to secure a Ruby on Rails API with authentication using JWT and how to consume it in a React application. We used the devise-jwt
gem to implement JWT authentication in the Rails API and the jwt
library to generate and verify JWT tokens. We also used the axios
library to make HTTP requests to the API and the js-cookie
library to manage cookies in the browser.
We created a Rails API with a User
model and implemented authentication using JWT with devise-jwt
. We also created an authentication controller to handle the sign-in and sign-out actions.
In the React application, we created an api.js
file to define the API endpoints and used the Authorization
header to send the JWT token with the requests. We also created an AuthContext.js
file to manage the user authentication state and used the js-cookie
library to store the JWT token in the browser. Finally, we created a Posts.js
file to display the posts retrieved from the API.
Securing a web application with authentication is a crucial aspect of web development. By using JWT, we can create secure and scalable APIs that can be consumed by different clients. The setup of a web application with a separate frontend and backend requires careful consideration of security aspects and best practices to ensure the protection of user data and prevent attacks. With the tools and techniques presented in this blog post, developers can implement secure and robust authentication systems in their Ruby on Rails and React applications.