Skip to Content
Course content

129: JWT Authentication in Rails APIs

Click on the "Edit" button in the top corner of the screen to edit your slide content.

You've just finished setting up your JWT (JSON Web Token) logic. You have a method to encode a user ID into a token, and your login endpoint is successfully spitting out long, encrypted-looking strings. But as soon as you try to use that token to access a protected route, everything falls apart.

# app/controllers/application_controller.rb
def authenticate_request
  header = request.headers['Authorization']
  decoded = JWT.decode(header, Rails.application.secrets.secret_key_base)[0]
  @current_user = User.find(decoded['user_id'])
rescue JWT::DecodeError
  render json: { error: 'Unauthorized' }, status: :unauthorized
end

If you test this in Postman or Insomnia by sending the header Authorization: Bearer eyJhbG..., you'll get a 401 Unauthorized every single time. You check your secret key, you check your payload—everything looks perfect. Why is it failing?

The "Bearer" String Trap

The problem is a classic "details matter" bug. In the HTTP spec, the Authorization header usually follows the format Bearer <token>. When you call request.headers['Authorization'], Rails gives you the entire string, including the word "Bearer" and the space after it.

The jwt gem doesn't know what "Bearer " is. It's expecting a base64-encoded string consisting of three parts separated by dots. When it sees the word "Bearer", it realizes the string isn't a valid JWT and throws a JWT::DecodeError. I've spent more hours than I'd like to admit debugging this because I forgot that the client sends the prefix, but the server only wants the payload.

Stripping the Prefix and Validating

To fix this, we need to isolate the actual token. The most robust way is to split the string or use a regex to grab everything after the first space. Here is how we handle it properly:

def authenticate_request
  header = request.headers['Authorization']
  
  # Check if the header exists and starts with 'Bearer '
  if header.nil? || !header.start_with?('Bearer ')
    return render json: { error: 'Missing or malformed token' }, status: :unauthorized
  end

  # Split "Bearer eyJhbG..." and take the second part
  token = header.split(' ').last
  
  decoded = JWT.decode(token, Rails.application.secrets.secret_key_base)[0]
  @current_user = User.find(decoded['user_id'])
rescue JWT::DecodeError, ActiveRecord::RecordNotFound
  render json: { error: 'Invalid or expired token' }, status: :unauthorized
end

Notice that I added ActiveRecord::RecordNotFound to the rescue block. This is crucial. If a user is deleted from your database but their JWT is still "valid" (not expired), User.find will throw an exception. You want that to be treated as an unauthorized request, not a 500 Internal Server Error.

Wiring it into the Controller Lifecycle

Now that we have a working authentication method, you don't want to manually call authenticate_request inside every single action. That's a recipe for forgetting one and leaving a security hole in your API.

Instead, use a before_action. I typically keep my ApplicationController lean and define the method there, then opt-in to protection in the specific controllers that need it. For example, in a PostsController, you might want anyone to be able to read posts, but only authenticated users to create them:

class PostsController < ApplicationController
  # Only protect these specific actions
  before_action :authenticate_request, only: [:create, :update, :destroy]

  def index
    render json: Post.all
  end

  def create
    # @current_user is available here because of the before_action
    post = @current_user.posts.build(post_params)
    if post.save
      render json: post, status: :created
    else
      render json: post.errors, status: :unprocessable_entity
    end
  end
end

By using only: [...], you maintain the flexibility of a public API while ensuring your write operations are locked down. Just remember: if you decide to make the entire controller private, you can just use before_action :authenticate_request without the options hash.




📋 Practical Task

Implementing Token-Based Access for a Private Notes API

You are building a "Private Notes" API where users can store secret snippets of text. Currently, the NotesController is completely open, meaning anyone can create or delete any note.

Your Task:

  1. In ApplicationController, implement the authenticate_request method. It must:
    • Extract the token from the Authorization header (stripping the "Bearer " prefix).
    • Decode the token using Rails.application.secrets.secret_key_base.
    • Set an instance variable @current_user by finding the user associated with the user_id in the token payload.
    • Handle JWT::DecodeError and ActiveRecord::RecordNotFound by returning a 401 Unauthorized response.
  2. In NotesController, add a before_action to ensure that the index, show, and destroy actions are only accessible to authenticated users.
  3. Modify the index action in NotesController so that it only returns notes belonging to the @current_user, rather than Note.all.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.