0

Recently, I deployed a rails backend and a vue frontend to heroku. Everything seemed fine a few days ago, until I tried logging into the web app today. I am getting 401 responses immediately after because after you login, the app is supposed to display your comic books from database.

For background, I am following a Vue.js/Rails tutorial on youtube by webcrunch. In the tutorial, localStorage is used along with JWTSessions and axios. I used the following question, but it didn't offer the answer I was looking for HttpGet 401 status code followed by 200 status code. I also tried adding trailing slashes to my api urls, but that did not fix the issue. In the vue frontend, the tutorial also made use of a axios/index.js file that configures instances of axios requests.

application_controller.rb

class ApplicationController < ActionController::Base
  include JWTSessions::RailsAuthorization
  rescue_from JWTSessions::Errors::Unauthorized, with: :not_authorized

  private

    # access current user
    def current_user
      @current_user ||= User.find(payload['user_id'])
    end

    def not_authorized
      # render actual data back since no view
      render json: { error: 'Not authorized' }, status: :unauthorized
    end
end

signin_controller.rb (Not including signup_controller.rb because they are similar)

class SigninController < ApplicationController
  before_action :authorize_access_request!, only: [:destroy]
  skip_before_action :verify_authenticity_token

  def create
    user = User.find_by!(email: params[:email])
    if user.authenticate(params[:password])
      payload = { user_id: user.id }
      # allow user to log in again if failed attempt
      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
      tokens = session.login
      response.set_cookie(JWTSessions.access_cookie,
                        value: tokens[:access],
                        httponly: true,
                        secure: Rails.env.production?)
      render json: { csrf: tokens[:csrf] }
    else
      not_authorized
    end
  end

  def destroy
    session = JWTSessions::Session.new(payload: payload)
    session.flush_by_access_payload
    render json: :ok
  end

  private

  def not_found
    render json: { error: "Cannot find email/password combination" }, status: :not_found
  end
end

/config/initializers/cors.rb

Rails.application.config.middleware.insert_before ActionDispatch::Static, Rack::Cors do
  allow do
    origins ['https://gem-mint-client.herokuapp.com', 'http://gem-mint-client.herokuapp.com']

    resource '*',
      headers: :any,
      credentials: true,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

/frontend/src/backend/axios/index.js

import axios from 'axios'

const API_URL = 'https://gem-mint-server.herokuapp.com'

const securedAxiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  }
})

const plainAxiosInstance = axios.create({
  baseURL: API_URL,
  withCredentials: true,
  headers: {
    'Content-Type': 'application/json'
  }
})

securedAxiosInstance.interceptors.request.use(config => {
  const method = config.method.toUpperCase()
  if (method !== 'OPTIONS' && method !== 'GET') {
    config.headers = {
      ...config.headers,
      'X-CSRF-TOKEN': localStorage.csrf
    }
  }
  return config
})

securedAxiosInstance.interceptors.response.use(null, error => {
  if (error.response && error.response.config && error.response.status === 401) {
    return plainAxiosInstance.post('/refresh', {}, { headers: { 'X-CSRF-TOKEN': localStorage.csrf } })
      .then(response => {
        localStorage.csrf = response.data.csrf
        localStorage.signedIn = true
        let retryConfig = error.response.config
        retryConfig.headers['X-CSRF-TOKEN'] = localStorage.csrf
        return plainAxiosInstance.request(retryConfig)
      }).catch(error => {
        delete localStorage.csrf
        delete localStorage.signedIn

        location.replace('/')
        return Promise.reject(error)
      })
  } else {
    return Promise.reject(error)
  }
})

export { securedAxiosInstance, plainAxiosInstance }

I was dealing with CORS issues, but I don't think that is the case with this issue. My CORS issue was because I inputed my url without https in the browser header, so to fix that I include both https and http url as origins. The end result that I am expecting is that you should be able to login or sign up without being immediately logged out due to not being authorized. If more code is need, please let me know. Thank you in advance.

Edit2: comics_controller.rb

module Api
  module V1
    class ComicsController < ApplicationController
      before_action :authorize_access_request!
      before_action :set_comic, only: [:show, :edit, :update, :destroy]
      skip_before_action :verify_authenticity_token

      # GET /comics
      # GET /comics.json
      def index
        @comics = current_user.comics.all

        render json: @comics
      end

      # GET /comics/marvel
      def marvel
        # start connection to Marvel
        @client = Marvel::Client.new

        # keys to configure marvel api
        @client.configure do |config|
          config.api_key = ENV['PUBLIC_MARVEL_API']
          config.private_key = ENV['PRIVATE_MARVEL_API']
        end

        # store parameters to send as request
        # set limit to 100, otherwise server error if over
        @args = {limit: '100'}

        # marvel api creator parameter needs to be inputted as an id
        # find creator id
        if params[:creators] != ''
          @creator = @client.creators(nameStartsWith: params[:creators])
          @args[:creators] = @creator[0][:id]
        end

        # check if other params were inputted and add to @args object
        if params[:issueNumber] != ''
          @args[:issueNumber] = params[:issueNumber]
        end
        if params[:title] != ''
          @args[:titleStartsWith] = params[:title]
        end
        if params[:startYear] != ''
          @args[:startYear] = params[:startYear]
        end

        # find comics based on given parameters
        @comics = @client.comics(@args)

        render json: @comics
      end

      # GET /comics/price
      def price
        EbayRequest.configure do |config|
          config.appid = ENV['EBAY_APP_PROD_ID']
          config.certid = ENV['EBAY_CERT_PROD_ID']
          config.devid = ENV['EBAY_DEV_PROD_ID']
          config.runame = ENV['EBAY_PROD_RUNAME']
          config.sandbox = false
        end
        @items = EbayRequest::Finding.new.response('findItemsAdvanced', categoryId: '63', keywords: params[:query])

        render json: @items
      end

      # GET /comics/1
      # GET /comics/1.json
      def show
        @comic
        render json: @comic
      end

      # GET /comics/new
      def new
        @comic = Comic.new
      end

      # GET /comics/1/edit
      def edit
      end

      # POST /comics
      # POST /comics.json
      def create
        @comic = current_user.comics.build(comic_params)

        respond_to do |format|
          if current_user.comics.exists?(title: @comic.title)
            # access first object
            found_book = current_user.comics.where(title: @comic.title)[0]
            quantity = found_book.quantity
            # sum up previous quantity with additional quantity
            found_book.update_attribute(:quantity, quantity + @comic.quantity)
            format.html { redirect_to '/', notice: 'Invite was successfully created' }
            format.json {
              render json: @comic, status: :created, location: api_v1_comic_url(@comic)
            }
          else
            # if no records exist, create a new one
            if @comic.save
              format.html { redirect_to '/', notice: 'Invite was successfully created' }
              format.json {
                render json: @comic, status: :created, location: api_v1_comic_url(@comic)
              }
            else
              format.html { render :new }
              format.json {
                render json: @comic.errors, status: :unprocessable_entity
              }
            end
          end
        end
      end

      # PATCH/PUT /comics/1
      # PATCH/PUT /comics/1.json
      def update
        respond_to do |format|
          if @comic.update(comic_params)
            format.html
            format.json {
              render json: @comic, status: :created, location: api_v1_comic_url(@comic)
            }
          else
            format.html { render :new }
            format.json {
              render json: @comic.errors, status: :unprocessable_entity
            }
          end
        end
      end

      # DELETE /comics/1
      # DELETE /comics/1.json
      def destroy
        @comic.destroy
      end

      private

        # Use callbacks to share common setup or constraints between actions.
        def set_comic
          @comic = current_user.comics.find(params[:id])
        end

        # Never trust parameters from the scary internet, only allow the white list through.
        def comic_params
          params.require(:comic).permit(:title, :issue, :year, :image, {:creators => []}, :quantity, :description)
        end
    end
  end
end

Edit: Log output

at=info method=POST path="/signin" host=gem-mint-server.herokuapp.com request_id=a6f30e31-8f19-4c7f-9381-4135efb0f648 fwd="75.18.100.151" dyno=web.1 connect=1ms service=307ms status=200 bytes=1162 protocol=https
2019-08-07T21:15:03.882596+00:00 app[web.1]: I, [2019-08-07T21:15:03.882487 #4]  INFO -- : [a6f30e31-8f19-4c7f-9381-4135efb0f648] Completed 200 OK in 303ms (Views: 0.2ms | ActiveRecord: 1.6ms)
2019-08-07T21:15:04.170728+00:00 heroku[router]: at=info method=GET path="/api/v1/comics" host=gem-mint-server.herokuapp.com request_id=3afecf82-2c42-4e2c-9f25-cffeb88b5017 fwd="75.18.100.151" dyno=web.1 connect=1ms service=4ms status=401 bytes=736 protocol=https
2019-08-07T21:15:04.170050+00:00 app[web.1]: I, [2019-08-07T21:15:04.169938 #4]  INFO -- : [3afecf82-2c42-4e2c-9f25-cffeb88b5017] Started GET "/api/v1/comics" for 75.18.100.151 at 2019-08-07 21:15:04 +0000
2019-08-07T21:15:04.170902+00:00 app[web.1]: I, [2019-08-07T21:15:04.170835 #4]  INFO -- : [3afecf82-2c42-4e2c-9f25-cffeb88b5017] Processing by Api::V1::ComicsController#index as HTML
2019-08-07T21:15:04.171594+00:00 app[web.1]: I, [2019-08-07T21:15:04.171528 #4]  INFO -- : [3afecf82-2c42-4e2c-9f25-cffeb88b5017] Completed 401 Unauthorized in 1ms (Views: 0.2ms | ActiveRecord: 0.0ms)
2019-08-07T21:15:04.373727+00:00 app[web.1]: I, [2019-08-07T21:15:04.373626 #4]  INFO -- : [82467937-7dbc-4cf2-9cf1-c010f92fad41] Started POST "/refresh" for 75.18.100.151 at 2019-08-07 21:15:04 +0000
2019-08-07T21:15:04.374995+00:00 app[web.1]: I, [2019-08-07T21:15:04.374936 #4]  INFO -- : [82467937-7dbc-4cf2-9cf1-c010f92fad41] Processing by RefreshController#create as HTML
2019-08-07T21:15:04.375091+00:00 app[web.1]: I, [2019-08-07T21:15:04.375032 #4]  INFO -- : [82467937-7dbc-4cf2-9cf1-c010f92fad41]   Parameters: {"refresh"=>{}}
2019-08-07T21:15:04.376098+00:00 app[web.1]: I, [2019-08-07T21:15:04.376018 #4]  INFO -- : [82467937-7dbc-4cf2-9cf1-c010f92fad41] Completed 401 Unauthorized in 1ms (Views: 0.3ms | ActiveRecord: 0.0ms)
  • To start helping you, we will need to know what is the GET request that leads to a 401. You can also pinpoint the issue further by reading Rails' logs, to see which line exactly triggers the 401 – Matthieu Libeer Aug 07 '19 at 19:22
  • Hey there Matthieu, apologies for a late reply. I know the issue happens at the GET request of /api/v1/comics. This is happening with the base api url of 'https://gem-mint-server.herokuapp.com'. I used heroku logs to get the following output. I'll add the output of logs to the original post. – JMcClane139 Aug 07 '19 at 21:25
  • Can you show the ComicsController, since that is where the you are getting the 401 from? – Doughtz Aug 08 '19 at 03:15
  • Hey there! I just edited to include ComicsController. Sorry if format is off. I pasted code from phone – JMcClane139 Aug 08 '19 at 03:36
  • Hi there, I use the same project Vue.js/Rails tutorial on YouTube by webcrunch, and facing the same issue. Did you resolve it? – Mingo May 22 '21 at 15:32

1 Answers1

0

I use the same source project and faced the same problem, but it only happens in my production environment. It is ok in the development environment. I figured it out the problem is in the signin_controller.rb

 if user.authenticate(params[:password])
      payload = { user_id: user.id }
      session = JWTSessions::Session.new(payload: payload, refresh_by_access_allowed: true)
      tokens = session.login
      response.set_cookie(JWTSessions.access_cookie,
                          value: tokens[:access],
                          httponly: true,
                          secure: Rails.env.production?)
                          )
      render json: { csrf: tokens[:csrf] }

Since my site is not using HTTPS, I change secure: Rails.env.production? to secure: false, then it works fine.

Mingo
  • 1,613
  • 2
  • 16
  • 20