Introduction
Before starting our short journey, you need to have Redis and Ruby on your machine.
Generating JWTs that Hold User IDs
In order to generate JWTs in Ruby, I used the ruby-jwt
gem. Here is how you can create one:
def encode_jwt(user_id)
payload = { user_id: user_id, session_id: SecureRandom.hex(4) }
JWT.encode payload, 'my_secret', "HS256"
end
encode_jwt
is a function that accepts user_id
, initializes the payload with it, and returns the generated token using the encode
function from the JWT module.
Storing Generated Tokens in Redis
After creating our token, we need to store it in Redis as sessions. First, initialize a Redis variable to make it globally accessible. To do this, add the redis
gem to your Gemfile or install it with:
gem install redis
Next, create a file called redis.rb
under config/initializers
and define a global variable like this:
$redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost:6379/0", password: "password")
This initializes a Redis client instance with the given credentials. Now, store the token in Redis:
def save_jwt_to_redis(user_id, jwt)
r_key = "user_#{user_id}_#{jwt}"
$redis.set(r_key, jwt)
$redis.expire(r_key, 30.days.to_i)
end
The save_jwt_to_redis
function creates a key (r_key
) using user_id
and jwt
, then sets the JWT with this key in Redis. To ensure sessions expire, we use Redis’ expire
function to set an expiration date for the unique key.
Putting It All Together
Let’s register some users and hold their sessions using the functions defined above. Here’s the create
function in our UserController
:
def create
user = User.new(user_params)
if user.save
token = encode_jwt(user.id)
save_jwt_to_redis(user.id, token)
render json: { success: true, token: token }, status: :created
else
render json: { success: false, errors: user.errors.messages }, status: :bad_request
end
end
This initializes a user with the given user_params
. If the save is successful, it generates a token and saves that token to Redis.
Securing Endpoints (Decoding Token and Fetching from Redis)
Let’s secure some endpoints and restrict unauthorized access. We create a filter method called auth_with_jwt
and call it before each secured endpoint:
def auth_with_jwt
token = request.headers["Authorization"]&.split(" ")&.last || params[:token]
unless token
render json: { error: "JWT token not found." }, status: :unauthorized
return
end
begin
decoded = JWT.decode token, "my_secret", true, algorithm: "HS256"
user_id = decoded.first["user_id"]
jwt_token = get_jwt_from_redis(user_id, token)
unless jwt_token
raise JWT::DecodeError
end
@current_user = User.find(user_id)
rescue JWT::DecodeError => e
logger.error "#{e.message}"
render json: { error: "Invalid JWT token." }, status: :unauthorized
rescue JWT::ExpiredSignature => e
logger.error "#{e.message}"
render json: { error: "Expired JWT token." }, status: :unauthorized
rescue JWT::InvalidIssuerError => e
render json: { error: "Invalid JWT issuer." }, status: :unauthorized
rescue JWT::InvalidAudError => e
render json: { error: "Invalid JWT audience." }, status: :unauthorized
rescue ActiveRecord::RecordNotFound => e
render json: { error: "User not found." }, status: :not_found
end
end
This function extracts the token from the Authorization
header, decodes it to retrieve the user_id
, and fetches the token from the Redis session. If a token exists, it sets the @current_user
; otherwise, it raises an error.
Here’s how the get_jwt_from_redis
function works:
def get_jwt_from_redis(user_id, jwt)
r_key = "user_#{user_id}_#{jwt}"
$redis.get(r_key)
end
We use the same key format as in save_jwt_to_redis
to match keys and fetch the value (token) from Redis. If the key doesn’t exist or has expired, it returns nil
, and the user cannot be authorized.
Conclusion
Unfortunately, our journey ends here. Thank you for reading! I’ll be waiting for your questions and feedback. 🎉