Lets store our JWTs as sessions using Redis and Ruby on Rails.
Aykut Alp
Generating JWTs that holds 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, in order to init our payload with it and return the generated token using encode function from JWT module.
Storing generated tokens in Redis
We’ve created our token and we need to store it in Redis as sessions. First, we have to initialize a redis variable in order to reach globally. To do this you need to add redis gem to your Gemfile or just install with:
gem install redis
We have to create a file called redis.rb under config/initializers and create a global variable like this:
$redis = Redis.new(url: ENV["REDIS_URL"] || "redis://localhost:6379/0", password: "password")
This will initialize a Redis client instance with given credentials. For the next we have to store our 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
save_jwt_to_redis function creates a key called r_key using user_id and jwt and sets our jwt with this key to redis. Because we want users’ sessions to expire after a while so we just used Redis’ expire function and set an expire date for our unique key.
Putting all together
Lets register some users and hold their sessions using the functions defined above. To do that we have a function called create in our UserController and looks like this:
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
It will initialize a user with the given user_params, and if db save is successful it will generate a token and save that token to redis.
Securing endpoints (decoding token and get from redis.)
Lets say, we want to secure and restrict unauthorized access to some of our endpoints. To do that we can create a filter method called auth_with_jwt and call it before each endpoint we want to secure.
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
It is a bit huge function, and basically it extracts the token from the Authorization header and decodes the token in order to get user_id after that it gets the token from Redis, if there is a token, no problem just sets the current_user, otherwise it raises an error.
As you noticed we’ve used get_jwt_from_redis function in order to get our token from Redis session. Here how it 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 with save_jwt_to_redis function in order to match our keys properly and get the value(token) from Redis. If there is no key or our key is expired and deleted from Redis it will return nil and we cannot be authorized anymore.
Unfortunately our journey ends here, thank you for reading and I will be waiting for your questions and feedback 🎉.