class JwtService
ACCESS_TOKEN_LIFETIME = 15.minutes
REFRESH_TOKEN_LIFETIME = 7.days
SECRET = Rails.application.credentials.jwt_secret
def self.encode_access_token(user_id)
payload = {
user_id: user_id,
exp: ACCESS_TOKEN_LIFETIME.from_now.to_i,
jti: SecureRandom.uuid
}
JWT.encode(payload, SECRET, 'HS256')
end
def self.encode_refresh_token(user_id)
jti = SecureRandom.uuid
payload = {
user_id: user_id,
exp: REFRESH_TOKEN_LIFETIME.from_now.to_i,
jti: jti,
type: 'refresh'
}
token = JWT.encode(payload, SECRET, 'HS256')
Rails.cache.write("refresh_token:#{jti}", user_id, expires_in: REFRESH_TOKEN_LIFETIME)
token
end
def self.decode(token)
JWT.decode(token, SECRET, true, algorithm: 'HS256').first
rescue JWT::DecodeError
nil
end
def self.revoke_refresh_token(jti)
Rails.cache.delete("refresh_token:#{jti}")
end
end
Stateless authentication with JWT tokens simplifies horizontal scaling but introduces security concerns around token lifetime and revocation. I use short-lived access tokens (15 minutes) combined with longer-lived refresh tokens stored in an encrypted HTTP-only cookie. When the access token expires, the client can request a new one using the refresh token without forcing the user to re-authenticate. The refresh token is stored in Redis with a blacklist mechanism so I can revoke sessions immediately when needed. This approach balances security and user experience—users stay logged in across sessions but compromised access tokens have limited blast radius. I also include a jti (JWT ID) claim to enable granular revocation.
Related snips
class CommentsController < ApplicationController
before_action :set_post
def create
@comment = @post.comments.build(comment_params)
System test: asserting Turbo Stream responses
class Post < ApplicationRecord
belongs_to :author, class_name: 'User'
has_many :comments, dependent: :destroy
scope :published, -> { where.not(published_at: nil).where('published_at <= ?', Time.current) }
scope :draft, -> { where(published_at: nil) }
ActiveRecord scopes for reusable query logic
payload = {
sub: user.id,
iss: 'https://auth.example.com',
aud: 'codesnips-api',
exp: 15.minutes.from_now.to_i,
iat: Time.now.to_i,
JWT issuance and verification without common footguns
module Api
module V1
class UsersController < BaseController
def show
user = User.includes(:profile).find(params[:id])
ETags for conditional requests and caching
class PostsController < ApplicationController
def index
@posts = Post.includes(:author)
.order(created_at: :desc)
.page(params[:page])
.per(10)
Turbo Frames: infinite scroll with lazy-loading frame
require "csv"
class PeopleCsvStream
include Enumerable
HEADERS = %w[id full_name email signed_up_at plan].freeze
Resilient CSV Export as a Streamed Response
Share this code
Here's the card — post it anywhere.