I have this code in my ApplicationController
class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
def authenticate!
# unless current_user
if current_user
current_user
else
render json: { 'error' => {'message' => 'Invalid access token', 'code' => 301 } }
return
end
end
def current_user
return #current_user if #current_user.present?
user = User.find_by(access_token: params.delete(:token))
if user.present?
#current_user = user
else
false
end
end
end
and I authenticate a user with
class Api::V1::RegisterController < ApplicationController
layout nil
skip_before_action :verify_authenticity_token
def get_user
authenticate!
render json: {'hello' => 'hi'}
end
end
it throws me an error of Double Render.
how can I render an invalid access token message if user's access token is not present in my database and return user details if present?
EDIT1: I tried the code provided by #andrew21
class ApplicationController < ActionController::Base
class UnauthorizedAccess < StandardError; end
rescue_from UnauthroizedAccess, with: render_invalid_access
protect_from_forgery with: :exception
def authenticate!
raise UnauthorizedAccess, 'invalid access token' unless current_user
end
def render_invalid_access
render json: { 'error' => {'message' => 'Invalid access token', 'code' => 301 } }
end
end
but I get an error.
undefined local variable or method `render_invalid_access' for ApplicationController:Class
why don't you raise an error on invalid access, then rescue the error and render the appropriate response. e.g.:
class ApplicationController < ActionController::Base
class UnauthorizedAccess < StandardError; end
protect_from_forgery with: :exception
rescue_from UnauthorizedAccess, with: :render_invalid_access
def authenticate!
raise UnauthorizedAccess, 'invalid access token' unless current_user
end
def render_invalid_access
render json: { 'error' => {'message' => 'Invalid access token', 'code' => 301 } }
end
end
Related
I'm building the token authentication for the rails side of the project project. which uses devise and JWT gems. I need to write a method(in session controller) to destroy the user session. Does anyone know how to go about doing this? in the front end the token is held in sessions when the user is logged in.
class SessionsController < Devise::SessionsController
# protect_from_forgery with: :null_session, if: ->{request.format.json?}
# skip_before_action :verify_authenticity_token
def create
user = User.find_by_email(params[:email])
if user && user.valid_password?(params[:password])
#current_user = user
else
render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
end
end
def destroy
# stuck here
end
end
here's the application controller too
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
respond_to :json
before_action :underscore_params!
before_action :configure_permitted_parameters, if: :devise_controller?
before_action :authenticate_user
private
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
end
def authenticate_user
if request.headers['Authorization'].present?
authenticate_or_request_with_http_token do |token|
begin
jwt_payload = JWT.decode(token, Rails.application.secrets.secret_key_base).first
#current_user_id = jwt_payload['id']
rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError
head :unauthorized
end
end
end
end
def underscore_params!
params.deep_transform_keys!(&:underscore)
end
def authenticate_user!(options = {})
head :unauthorized unless signed_in?
end
def current_user
#current_user ||= super || User.find(#current_user_id)
end
def signed_in?
#current_user_id.present?
end
end
I have the following namespaces ApiController
class Api::ApiController < ApplicationController
skip_before_action :verify_authenticity_token,
if: Proc.new { |c| c.request.content_type == 'application/json' }
before_action :authenticate
attr_reader :current_user
private
def authenticate
#current_user = AuthorizeApiRequest.call(request.headers).result
render json: { error: 'Not Authorized' }, status: 401 unless #current_user
end
end
On AuthorizeApiRequest.call, Rails complains that:
uninitialized constant Api::ApiController::AuthorizeApiRequest
My AuthorizeApiRequest class is defined under app/commands:
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
def decoded_auth_token
#decoded_auth_token ||= JsonWebToken.decode(http_auth_header)
end
def http_auth_header
if headers['Authorization'].present?
return headers['Authorization'].split(' ').last
else
errors.add(:token, 'Missing token')
end
nil
end
end
So it seems to not allow me to call AuthorizeApiRequest.call without added namespace to front. How to fix?
Your app/commands folder doesn't seem to be loaded into Rails at boot.
You need to include your app/commands in your autoload paths for this to work or require the file manually in your controller.
See: https://guides.rubyonrails.org/autoloading_and_reloading_constants.html#autoload-paths
I'm trying to set up an API that logs in a user to my application and authenticates the user with a token. For this I am creating a new session.
Sessions controller
class Api::V1::SessionsController < Api::V1::BaseController
def create
user = User.where(email: params[:email]).first
if user&.valid_password?(params[:password])
render json: user.as_json(only: [:first_name, :last_name, :email]), status: :created
else
head(:unauthorized)
end
end
Base controller
class Api::V1::BaseController < ActionController::API
include Pundit
after_action :verify_authorized, except: :index
after_action :verify_policy_scoped, only: :index
rescue_from StandardError, with: :internal_server_error
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def internal_server_error(exception)
if Rails.env.development?
response = { type: exception.class.to_s, message: exception.message, backtrace: exception.backtrace }
else
response = { error: "Internal Server Error" }
end
render json: response, status: :internal_server_error
end
def user_not_authorized(exception)
render json: {
error: "Unauthorized #{exception.policy.class.to_s.underscore.camelize}.#{exception.query}"
}, status: :unauthorized
end
def not_found(exception)
render json: { error: exception.message }, status: :not_found
end
end
When I test the method, I receive AbstractController::DoubleRenderError, despite the user existing in the database. It points to the last line of the internal_server_error(exception) method of the BaseController.
Can someone help me understand why this code doesn't render the JSON of the user, when the user actually does exist in the database?
In this post, the errors are rescued in the both api and base controller methods. But it might not be best approach to handle errors because of some reasons are:
Fat Controllers
DRY
Maintainability
In ActionController::Base, we handled ActiveRecord::RecordNotFound in only ApplicationController. But for ActionController::API i have to rescue ActiveRecord::RecordNotFound in every controller. So are there any best approach for handle this problem?
Using Rails 5 and 'active_model_serializers' gem for api
ActionController::API
module Api
module V1
class UsersController < ActionController::API
before_action :find_user, only: :show
def find_user
#user = User.find(params[:id])
rescue ActiveRecord::RecordNotFound => e
render json: { error: e.to_s }, status: :not_found
end
end
end
end
ActionController::Base
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
private
def record_not_found
render file: "#{Rails.root}/public/404", layout: true, status: :not_found
end
end
You can do something like this in application_controller.rb
if Rails.env.production?
rescue_from ActiveRecord::RecordNotFound, with: :render_404
end
def render_404
render json: {meta: meta_response(404, "Record not found")}
end
This would rescue all RecordNotFound exception with 404 but only in production mode.
ActionController::API includes the ActionController::Rescue module which is what provides the rescue_from class method.
I would create an Api::BaseController base class that the Api::V1::UsersController can use instead of using ActionController::API on each controller class. This would allow you have a rescue_from in a single place instead of needing a rescue block on every action.
module Api
class BaseController < ActionController::API
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
module V1
class UsersController < BaseController
def find_user
#user = User.find(params[:id])
end
end
end
end
I'd also further create an Api::V1::BaseController to allow for easier versioning of the APIs. Then, if you decide to change the format of the errors for v2, just move the rescue_from in the Api::BaseController to the Api::V1::BaseController, and add a new rescue_from to the new Api::V2::BaseController.
module Api
class CommonBaseController < ActionController::API
# code common across API versions
end
module V1
class BaseController < CommonBaseController
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
render json: { error: e.to_s }, status: :bad_request
end
end
end
module V2
class BaseController < CommonBaseController
# use a custom base error class to make catching errors easier and more standardized
rescue_from BaseError, with: :handle_error
rescue_from ActiveRecord::RecordNotFound, with: :handle_error
private
def handle_error(e)
status, status_code, code, title, detail =
if e.is_a?(ActiveRecord::RecordNotFound)
[:not_found, '404', '104', 'record not found', 'record not found']
else
[
e.respond_to?(:status) ? e.status : :bad_request,
e.respond_to?(:status_code) ? e.status_code.to_s : '400',
e.respond_to?(:code) ? e.code.to_s : '100',
e.respond_to?(:title) ? e.title : e.to_s,
e.respond_to?(:detail) ? e.detail : e.to_s
]
end
render(
json: {
status: status_code,
code: code,
title: title,
detail: detail
},
status: status
)
end
end
end
end
Is there any chance to access authenticate Doorkeeper method from a rails action controller? I would like to skip authentication just for one of my actions('show') but if a specific condition aapplies I want to call the apiauthenticate method to do its job. So in the action 'show', first of all I check a condition, and if does not apply, then I need to activate the api_authenticate. I'm launching a test that should call api_authenticate and stop it there. But for some reason it keeps going on and it does not stop.
This is the code of my controller
skip_before_action :api_authenticate, only: :show
def show
param! :id, String, required: true
post = Services::Posts.find(params[:id])
if post.public
#post = post
#user_id = nil
else
api_authenticate
ap "it shouldnt get here if user is not logged in"
user = current_resource_owner
#post = Services::Posts.show(params[:id], user)
#user_id = user.identity rescue nil
end
end
#more actions....
And this is the api_controller.rb where I have the authenticate method
class ApiController < ApplicationController
protect_from_forgery with: :null_session
# Check the user is authenticated
before_action :api_authenticate
rescue_from ActionController::RoutingError, :with => :route_error
rescue_from ::AbstractController::ActionNotFound, :with => :action_error
rescue_from Exception, :with => :base_error if Rails.env.production?
def api_authenticate
doorkeeper_authorize!()
end
end
I have implemented something similar. Haven't tested the below code, but it should work.
skip_before_filter :doorkeeper_authorize! , only: :show
def show
param! :id, String, required: true
post = Services::Posts.find(params[:id])
if post.public
#post = post
#user_id = nil
else
doorkeeper_authorize!
ap "it shouldnt get here if user is not logged in"
user = current_resource_owner
#post = Services::Posts.show(params[:id], user)
#user_id = user.identity rescue nil
end
end
The Api controller,
class ApiController < ApplicationController
protect_from_forgery with: :null_session
# Check the user is authenticated
before_action :doorkeeper_authorize!
rescue_from ActionController::RoutingError, :with => :route_error
rescue_from ::AbstractController::ActionNotFound, :with => :action_error
rescue_from Exception, :with => :base_error if Rails.env.production?
def doorkeeper_unauthorized_render_options(error: nil)
response_hash = { status: false, description: error.description, expired: false }
response_hash[:expired] = true if error.description == "The access token expired"
{ json: response_hash }
end
end
If the problem persists, please add the params passed to the show action.