Trying to set up rails API, getting this error, both through console and in actual API requests:
Rack app error handling request { POST /login } #<NameError: uninitialized constant ActionText::Engine::ApplicationController
This is my ApplicationController:
class ApplicationController < ActionController::API
before_action :authorized
def encode_token(payload)
JWT.encode(payload, 's3cr3t')
end
def auth_header
#requesting the header type of authorization (with token) that we will declare through our api requests
# { Authorization: 'Bearer <token>' }
request.headers['Authorization']
end
def decoded_token
if auth_header
#going to take the token and decode it
# we're only concerned about the first index which will be a token with key of *user_id*
token = auth_header.split(' ')[1]
# header: { 'Authorization': 'Bearer <token>' }
begin
JWT.decode('s3cr3t', true, algorithm: 'HS256')
rescue JWT::DecodeError
nil
end
end
end
def logged_in_user
#consults decode_token to check the header for valid information
if decoded_token
user_id = decoded_token[0]['user_id']
#user = User.find_by(id: user_id)
end
end
def logged_in?
#returns true or false
!!logged_in_user
end
def authorized
#consults logged_in? see see if user is authorized
render json: { message: 'Please log in' }, status: :unauthorized unless logged_in?
end
end
Of course I would like to sort this error specifically (syntax error?) but not sure how to tackle general errors (beyond status codes) in Rails API. Is there a good practice I should be following?
Thanks!
For NameError: uninitialized constant ActionText::Engine::ApplicationController, where is your ApplicationController defined? It seems like ActionText requires it to be in app/controllers/application_controller.rb and possibly inherited from ActionController::Base.
As you develop an API I don't expect you need ActionText though and just accidentally load it. You should have a look in your config/application.rb file and look what gets loaded. If there is require 'rails/all' you should only load what you really need, e.g.
require "action_controller/railtie"
require "active_record/railtie"
but not sure how to tackle general errors (beyond status codes) in Rails API
In terms of general errors, you can e.g. use a rescue_from like this
class ApplicationController < ActionController::Base
rescue_from User::NotAuthorized, with: :deny_access # self defined exception
rescue_from ActiveRecord::RecordInvalid, with: :show_errors
rescue_from 'MyAppError::Base' do |exception|
render xml: exception, status: 500
end
private
def deny_access
...
end
def show_errors(exception)
exception.record.new_record? ? ...
end
end
https://apidock.com/rails/ActiveSupport/Rescuable/ClassMethods/rescue_from
For general errors it's not really needed to have a rescue_from StandardError as this is the default behaviour of Rails. Rails has a middleware called PublicExceptions which does (mostly) what you want so you can just let the StandardError propagate.
Instead of { error: "Internal Server Error" } it will render this
{
status: status,
error: Rack::Utils::HTTP_STATUS_CODES.fetch(status, Rack::Utils::HTTP_STATUS_CODES[500])
}
which in case of an exception will render { status: 500, error: "Internal Server Error" }. This should be a reasonable compromise.
For development you could think about adapting this middleware. You can set it with config.exceptions_app.
https://guides.rubyonrails.org/configuring.html#rails-general-configuration
https://api.rubyonrails.org/classes/ActionDispatch/PublicExceptions.html
https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/public_exceptions.rb#L14
Related
I am trying to raise an exception from application_controller file for any actions other than read
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
def authorize!(action)
raise NotAuthorized if action != :read && !current_user.admin?
true
end
end
But when I am making a POST request, the exception itself is throwing error. The following is the error.
NameError (uninitialized constant ApplicationController::NotAuthorized):
How can I fix this without having to add any new modules?
In rails there is no in-built exception called NotAuthorized, so when you try to raise this, rails doesn't know what to do. You can do this in two ways:
You can raise directly using built-in exceptions or create your own. Ex:
raise ActionController::RoutingError.new('Not authorized')
# or
raise ActionController::BadRequest.new('Not authorized')
# or
raise ActionController::MethodNotAllowed.new('Not authorized')
I recommend creating an external module and including it here, it keeps you code clean.
I have a Rails app that is in API mode.
Are there any gems that provide simple JWT authentication?
Yup, the standard JWT gem is great. Just add it to your gemfile:
gem 'jwt'
Then I use it as a concern that I can include like:
module JWTAuthenticatable
extend ActiveSupport::Concern
included do
before_action :authenticate_and_restrict_access, if: -> { %w[json csv].include? request.format }
rescue_from JWT::DecodeError, with: :invalid_token
rescue_from JWT::ExpiredSignature, with: :expired_token
def authenticate_and_restrict_access
return if current_user
authenticate_or_request_with_http_token do |authentication_token|
data = JSONWebToken.decode(authentication_token)
sign_in User.find(data[:id]), store: false
end
authenticate_user! if !current_user and request.format == 'html'
end
def invalid_token
render json: { errors: 'api.invalid_token' }, status: :unauthorized
end
def expired_token
render json: { errors: 'api.expired_token' }, status: :unauthorized
end
end
end
You should be able to tailor this to your needs.
use devise-jwt
here is an article I have written including all code you may need and GitHub repo that has a working code
I use pundit to handle my API policies, I have an item show that can be forbidden to user in some cases, and in other cases just restricted. By restricted I mean it's forbidden now, but if he pays he could access it then. So I need my API to respond with a specific code (402 Payment Required) so the client can invite the user to pay in order to unlock the show.
This is my current code, it only respond with 403 when pundit returns false.
Where would it be best to implement a condition to return 403 OR 402 in order to be DRY and clean?
class Api::V1::ItemController < Api::V1::BaseController
def show
#item = Item.find(params[:id])
authorize #item
end
end
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
# 403 will be generated, that's ok.
return false if !record.band.members.include?(user)
# If that condition is false I want to generate a 402 error at the end, not a 403.
user.premium?
end
end
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
# Here I've got the exception with :policy, :record and :query,
# also I can access :current_user so I could go for a condition,
# but that would include duplicated code from ItemPolicy#show?.
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
Unfortunately Pundit cannot handle different error types out of the box. And it is built to always expect the policy's methods to return true or false false. Therefore raising another custom error and rescuing from that in the controller will not work, because it would break view methods too.
I suggest a workaround to introduce different error types. Something like this might work:
# in the policy
class ItemPolicy < ApplicationPolicy
def show?
return true if record.public?
return false unless record.band.members.include?(user)
if user.premium?
true
else
Current.specific_response_error_code = :payment_required
false
end
end
end
# in the controller
class Api::V1::BaseController < ActionController::API
include Pundit
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
def user_not_authorized(_exception)
case Current.specific_response_error_code
when :payment_required
render json: { error: { message: "Premium required" } }, status: :payment_required
else
render json: { error: { message: "Access denied" } }, status: :forbidden
end
end
end
I would not consider using the global CurrentAttributes a good practice but they are part of Rails and in this case, using this global data store avoids overriding pundit internals.
You might want to read the API docs about CurrentAttributes.
create Response module in app/controllers/concerns/response.rb
module Response
def json_response(object, status = :ok)
render json: object, status: status
end
end
create ExceptionHandler in app/controllers/concerns/exception_handler.rb
module ExceptionHandler
extend ActiveSupport::Concern
included do
rescue_from Pundit::NotAuthorizedError, with: :unauthorized_request
end
private
# JSON response with message; Status code 401 - Unauthorized
def unauthorized_request(e)
json_response({ message: e.message }, :unauthorized)
end
end
in app/controllers/application_controller.rb
class ApplicationController < ActionController::API
include Response
include ExceptionHandler
end
THAT'S IT
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
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
I'm developing a REST web service in Ruby on Rails.
After each request I would like to store in the database the response HTTP status code even in presence of some exception. How can I do that?
I have done these two attempts without success:
after_filter in application controller
class Api::ApiController < ActionController::Base
before_action :set_current_rest_request
after_filter :finalize_current_rest_request
private
def set_current_rest_request
#current_rest_request = RestRequest.new
#current_rest_request.request_at = DateTime.now
#current_rest_request.save
end
def finalize_current_rest_request
#current_rest_request.answer_at = DateTime.now
#current_rest_request.http_status_code = response.status
#current_rest_request.save
end
end
Doesn't work because finalize_current_rest_request is not called in case of exceptions
rescue_from in application controller
class Api::ApiController < ActionController::Base
before_action :set_current_rest_request
after_filter :finalize_current_rest_request
rescue_from Exception, :with => :handle_exception
private
def set_current_rest_request
#current_rest_request = RestRequest.new
#current_rest_request.request_at = DateTime.now
#current_rest_request.save
end
def finalize_current_rest_request
#current_rest_request.answer_at = DateTime.now
#current_rest_request.http_status_code = response.status
#current_rest_request.save
end
def handle_exception(exception)
finalize_current_rest_request
raise exception
end
end
Doesn't work because response.status is still 200 when I call finalize_current_rest_request inside handle_exception, before the raise of the exception
You need to wrap each action of the controller with a begin rescue block. Something like:
begin
respond_to do |format|
format.json { redirect_to foos_path, notice: 'Foo was successfully destroyed.' }
rescue
#current_rest_request.http_status_code = response.status
end
end
Normally behavior like this is left up to the logs but I am assuming you have a really good reason for doing this.
If you really want it to be dry as you mentioned you can put in your application controller:
rescue_from Exception, :with => :store_request
def store_request
current_rest_request = RestRequest.new
current_rest_request.request_at = DateTime.now
current_rest_request.http_status_code = response.status
current_rest_request.save
end
Note: It is often considered bad practice to blanket rescue in the application controller. I think the best way to actually handle this is to implement a comprehensive logging scheme.
I finally solved adding a middleware (here some datails on how rails middlewares work).
In detail, in config/application.rb, I added:
config.middleware.insert_before "Rails::Rack::Logger","StoreStatus"
the api controller was
class Api::ApiController < ActionController::Base
before_action :set_current_rest_request
private
def set_current_rest_request
#current_rest_request = RestRequest.new
request.env[:current_rest_request] = #current_rest_request
end
end
and I added the file lib/store_status.rb with the following code:
class StoreStatus
def initialize(app)
#app = app
end
def call(env)
data = #app.call(env)
if (env[:current_rest_request])
rest_request = env[:current_rest_request]
rest_request.http_status_code = data[0]
rest_request.save
end
data
end
end
Please notice that there may be some syntax error because this code has been obtained refactoring a code which contains other useless complexity for this question.