Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
129: JWT Authentication in Rails APIs
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:
- In
ApplicationController, implement theauthenticate_requestmethod. It must:- Extract the token from the
Authorizationheader (stripping the "Bearer " prefix). - Decode the token using
Rails.application.secrets.secret_key_base. - Set an instance variable
@current_userby finding the user associated with theuser_idin the token payload. - Handle
JWT::DecodeErrorandActiveRecord::RecordNotFoundby returning a 401 Unauthorized response.
- Extract the token from the
- In
NotesController, add abefore_actionto ensure that theindex,show, anddestroyactions are only accessible to authenticated users. - Modify the
indexaction inNotesControllerso that it only returns notes belonging to the@current_user, rather thanNote.all.
There are no comments for now.