Implementation of proper data validation for JWT access tokens
require 'jwt'
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user
private
def authenticate_request
@current_user = decode_authentication_command.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless @current_user
end
def decode_authentication_command
@decode_authentication_command ||= DecodeAuthenticationCommand.new
end
end
class DecodeAuthenticationCommand < Command
def initialize(headers = {})
@headers = headers
@errors = {}
end
def call
user_id = decoded_auth_token['user_id'] if decoded_auth_token
User.find(user_id) if user_id
end
private
def decoded_auth_token
@decoded_auth_token ||= JWT.decode(http_auth_header, nil, false)[0]
end
def http_auth_header
if @headers['Authorization'].present?
return @headers['Authorization'].split(' ').last
end
@errors[:token] = 'Missing token'
end
end
In the above code, the Ruby on Rails application is using JWT (JSON Web Tokens) for authentication. The token is being sent in the 'Authorization' header of the HTTP request.
The
DecodeAuthenticationCommand
class is responsible for decoding the JWT. It does this in the
decoded_auth_token
method, where it uses the
JWT.decode
method.
The issue lies in the use of
JWT.decode(http_auth_header, nil, false)[0]
. The third parameter to
JWT.decode
is a boolean that specifies whether to validate the signature of the token. By setting this to
false
, the application is not validating the signature of the JWT.
This means that an attacker could modify the payload of the JWT, or even remove the signature entirely, and the application would still accept it as valid. This could potentially allow an attacker to impersonate any user, or to elevate their privileges.
require 'jwt'
class ApplicationController < ActionController::API
before_action :authenticate_request
attr_reader :current_user
private
def authenticate_request
@current_user = decode_authentication_command.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless @current_user
end
def decode_authentication_command
@decode_authentication_command ||= DecodeAuthenticationCommand.new
end
end
class DecodeAuthenticationCommand < Command
SECRET_KEY = Rails.application.secrets.secret_key_base.to_s
def initialize(headers = {})
@headers = headers
@errors = {}
end
def call
user_id = decoded_auth_token['user_id'] if decoded_auth_token
User.find(user_id) if user_id
end
private
def decoded_auth_token
@decoded_auth_token ||= JWT.decode(http_auth_header, SECRET_KEY, true, { algorithm: 'HS256' })[0]
rescue JWT::DecodeError => e
@errors[:token] = e.message
end
def http_auth_header
if @headers['Authorization'].present?
return @headers['Authorization'].split(' ').last
end
@errors[:token] = 'Missing token'
end
end
The updated code now includes signature validation for the JWT access token. The
JWT.decode
method now includes the
SECRET_KEY
and
true
as arguments, which means it will verify the signature of the JWT token using the
HS256
algorithm. If the signature is invalid or missing, a
JWT::DecodeError
will be raised and caught, and the error message will be stored in
@errors[:token]
.
The
SECRET_KEY
is retrieved from the Rails application secrets, which should be stored securely and not be accessible in the code or version control system. This key is used to sign the JWT token and to verify the signature of incoming JWT tokens.
The
http_auth_header
method remains the same, it retrieves the token from the 'Authorization' header of the incoming request. If the header is missing, an error message is stored in
@errors[:token]
.
The
call
method retrieves the
user_id
from the decoded JWT token (if it exists) and finds the corresponding User. If the
user_id
is not present or if the User does not exist, the method will return
nil
.
The
authenticate_request
method in the
ApplicationController
calls the
decode_authentication_command
before every action. If the
@current_user
is not set (which means the JWT token was invalid or the User does not exist), it will render a 'Not Authorized' error message with a status of 401.