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.