3

I'm having trouble trying to authenticate a request spec. How would I pass a valid auth token in the header of each http request? Is my approach below the correct?

tweets_request_spec.rb

require 'rails_helper'

RSpec.describe 'Tweets API', type: :request do
  before do
    @tweets = create_list(:tweet, 10)
    @tweet = @tweets.first
  end

  describe 'GET /tweets' do
    before { get '/tweets', { "Authorization": *some sort of token*} }

    it "returns tweets" do
      expect(json).to_not be_empty
      expect(json).to eq(10)
    end

    it "is a successful http request" do
      expect(response).to have_http_response(200)
    end
  end
end

Here is my code for the authentication controller, as well as the modules that help with generating and decoding the auth tokens that are passed in the http headers.

authentication_controller.rb

class AuthenticationController < ApplicationController
  skip_before_action :authenticate_request

  def authenticate
    command = AuthenticateUser.call(params[:email], params[:password])

    if command.success?
      render json: { auth_token: command.result }
    else
      render json: { error: command.errors }, status: :authorized
    end
  end
end

authorize_api_request.rb

class AuthorizeApiRequest
  prepend SimpleCommand

  def initialize(headers = {})
    @headers = headers
  end

  def call
    user
  end

  private

  attr_reader :headers

  def user
    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token
    @user ||= errors.add(:token, 'Invalid token') && nil
  end

  #decode the auth token and retrieve the user id
  def decoded_auth_token
    @decoded_auth_token ||= JSONWebToken.decode(http_auth_header)
  end

  #retrieve auth token from header
  def http_auth_header
    if headers['Authorization'].present? 
      return headers['Authorization'].split(' ').last
    else
      errors.add(:token, 'Missing token')
    end
  end
end
Jas1997
  • 99
  • 1
  • 6

1 Answers1

5

Some Code Extracts copied from the the official pluralsight page

the endpoint to authenticate is in config/routes.rb

post 'authenticate', to: 'authentication#authenticate'

which executes this action. The action returns the token if you correctly authenticate.

def authenticate 
   command = AuthenticateUser.call(params[:email], params[:password]) 
   if command.success? 
      render json: { auth_token: command.result } 
   else 
      render json: { error: command.errors }, status: :unauthorized 
   end 
end

In rspec you have two options, you either mock this method or create a factory.

The concept of token based authentication is that once authenticated the user will have a token and by providing this token he will be able to access the functionalities only reserved to users

The request

$ curl -H "Content-Type: application/json" -X POST -d '{"email":"example@mail.com","password":"123123123"}' http://localhost:3000/authenticate

gives in response the token

{"auth_token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA"}

if you include in the header the token, the request will not trigger an authorization error

$ curl -H "Authorization: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE0NjA2NTgxODZ9.xsSwcPC22IR71OBv6bU_OGCSyfE89DvEzWfDU0iybMA" http://localhost:3000/items []

so before doing your get request, include in the request header the token

request.headers['Authorization'] = auth_token
get :your_action

How to provide a correct value of auth_token?

You will need to mock the method authenticate_request in ApplicationController, as it is called before the action

#app/controllers/application_controller.rb
class ApplicationController < ActionController::API
 before_action :authenticate_request
  attr_reader :current_user

  private

  def authenticate_request
    @current_user = AuthorizeApiRequest.call(request.headers).result
    render json: { error: 'Not Authorized' }, status: 401 unless @current_user
  end
end

I believe you should mock this line of code, to avoid receiving an authentication error.

@current_user = AuthorizeApiRequest.call(request.headers).result

so I would write the specs somethind like this

user = FactoryBot.create(:user)
allow(AuthorizeApiRequest).to receive(:call).and_return(user)
# request.headers['Authorization'] = auth_token # this is not required anymore the authentication is skipped
get :your_action

I quote pluralsight

By using before_action, the server passes the request headers (using the built-in object property request.headers) to AuthorizeApiRequest every time the user makes a request. Calling result on AuthorizeApiRequest.call(request.headers) is coming from SimpleCommand module where it is defined as attr_reader :result. The request results are returned to the @current_user, thus becoming available to all controllers inheriting from ApplicationController.

You can read more about mocking at

https://github.com/rspec/rspec-mocks

Fabrizio Bertoglio
  • 5,890
  • 4
  • 16
  • 57
  • Any idea why the mock line is giving me: Failure/Error: @current_user = AuthorizeApiRequest.call(request.headers).result NoMethodError: undefined method `result' for # # FrontendUser is my own custom class. – CyberMew Jun 26 '18 at 13:02
  • Solved it with https://stackoverflow.com/questions/40571911/mocking-chain-of-methods-in-rspec – CyberMew Jun 26 '18 at 13:10