Hi thanks for viewing my question. I'm building out a Rails API which works with a React front end on a different server. I'm using the Devise Token Auth gem, and am able to successfully log in, log out, and make get requests for multiple resources without any problems. To handle the changing tokens I am updating the headers on each request.
The problem I am running into is when I try to make a PUT request to update a resource, in which case I get a 401. Full error message:
Started PUT "/api/stores/3/orders/1" for 127.0.0.1 at 2017-07-12 16:00:05 -0400
Processing by Api::OrdersController#update as HTML
Parameters: {"order"=>{"id"=>1, "provider_notes"=>"tailor notes"}, "headers"=>{"client"=>"YVa0NIlxAdm6BLQXk0xeJw", "access-token"=>"bNc9BB0TgICIJzGfM4H_6A", "uid"=>"joe#joestailor.com"}, "store_id"=>"3", "id"=>"1"}
Can't verify CSRF token authenticity.
Filter chain halted as :authenticate_user! rendered or redirected
Completed 401 Unauthorized in 1ms (Views: 0.1ms | ActiveRecord: 0.0ms)
After checking, the access-token printed in the error message is in fact the token i got from the last request, so it should be good. Here's the controller I am working with. I'm not able to get passed the authenticate_user! before action.
class Api::OrdersController < ApplicationController
before_action :authenticate_user!
before_action :set_order, only: [:show, :update]
def index
render :json => current_user.store.open_orders.as_json(include: [:customer], methods: [:alterations_count])
end
def show
render :json => #order.as_json(include: [:customer, :items => {include: :item_type}])
end
def update
if #order.update(order_params)
render :json => #order
.as_json(include: [:customer, :items => {include: :item_type}])
else
byebug
end
end
private
def set_order
#order = Order.find(params[:id])
end
def order_params
if current_user.tailor?
params.require(order).permit(:requester_notes, :arrived, :fulfilled)
end
end
end
Any reason why a put request might work differently than the get requests I've been using (for this same controller)? Any advice to get around this with the Devise Auth Token would be awesome. Thanks.
are you using :authenticate_user! from devise?
if yes, it can't work for api
you need create your own helper method
you can put it in your api application_controller/model_controller
or cretae a module and include it wherever you need
then change the before action into authenticate_with_token!
def current_user_api
#current_user ||= User.find_by(auth_token: request.headers['Authorization'])
end
def user_signed_in_api?
current_user_api.present?
end
def authenticate_with_token!
render json: { errors: "Not authenticated" },
status: :unathorized unless user_signed_in_api?
end
this book chapter 5 will help you
http://apionrails.icalialabs.com/book/chapter_five
and for Can't verify CSRF token authenticity.
you can put this in your application controller
protect_from_forgery with: :null_session
skip_before_action :verify_authenticity_token, if: :json_request?
protected
def json_request?
request.format.json?
end
If you want to use :authenticate_user! or similar, then make sure to pass in all the necessary parameters:
access-token
expiry
token-type
uid
client
It looks like you were missing expiry and token-type.
This will result in the user being authenticated and the current_user being set as expected, assuming the token and credentials are valid.
Note: these headers my need to be exposed in your CORS configuration:
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource "*",
expose: %w[access-token expiry token-type uid client],
headers: :any,
methods: :any
end
end
Another Note: If you mounted your devise endpoint inside the api, then you will need to call a different :authenticate_user! method.
example:
namespace :api, defaults: { format: 'json' } do
namespace :v1 do
mount_devise_token_auth_for 'User', at: 'auth'
# API routing and resources
end
end
Then you would need to call :authenticate_api_v1_user! instead.
example:
class Api::V1::ApiController < ApplicationController
before_action :authenticate_api_v1_user!
end
Related
I'm having Rails web-app that has a form that pushes a request of a certain document into DB. A script on other machine should ask for newly created request. To handle that I've diced to add small JSON API inside the app.
For that purpose in routes I've added
defaults format: :json do
get "/api_name/get_request", to: "api_name#get_request"
end
After that I've met CSRF problem. And after googling I've found out that I need to add protect_from_forgery unless: -> { request.format.json? } in my controller. It looks like the code as follows
class ApiNameController < ApplicationController
protect_from_forgery unless: -> { request.format.json? }
def get_request
render json: {message: 'Received'}, status: :ok
end
end
After that in log I have
Started GET "/api_name/get_request" for ::1 at 2018-02-05 16:43:08 +0300
Processing by ApiNameController#get_request as JSON
Parameters: {"api_name"=>{}}
Completed 401 Unauthorized in 5ms (ActiveRecord: 0.0ms)
So the problem is still there. The questions are
What should I do to solve the problem?
Allowing JSON requests from any origin seem to be dangerous. How can I ad some protection? Maybe I should send some header from remote machine and check it inside verified_request ?
UPD I've changed comment inside get_request with render json: {message: 'Received'}, status: :ok and after that the result is the same
You have to skip devise before_actions, if its assigned in ApplicationController.
Controllers:
class ApiController < ApplicationController
protect_from_forgery unless: -> { request.format.json? }
skip_before_action :authenticate_user!, only: :get_request
def get_request
render json: {message: 'Received'}, status: :ok
end
end
Routes:
namespace :api, defaults: { format: :json } do
get 'get_request' => 'api#get_request'
end
To handle cross origin:
Rack Cors Gem Link
Rack Attack Gem Link
You probaly have some web authentication in ApplicationController in before_filter, since your ApiNameController inherits from it. That's why you receive Unauthorized response. So you should either stop inheriting from it, or exclude api controller methods from web-auth before-hook with some sort of:
class ApiNameController < ApplicationController
skip_before_action :authenticate_user!, only: :get_request
Speaking of security, you have at least two options:
token-based authentication
basic auth
The last one is less secure than token-based auth, but it's easier to implement, so it's up to you which one to chose.
Since devise doesn't support token authentication, I've been following this guide to develop my own authentication system. However, I noticed that if I pass authentication_token or access_token headers in my request, they probably get filtered in rails. Meaning that something like p request.headers['authentication_token'] and almost any of their variation returns nil. However, header named :omfgwork works. Can someone please explain why does this happen and link me to a source where I can find the list of filtered headers?
EDIT- I'm using postman to send the requests to my rails app.
EDIT 2
class ApplicationController < ActionController::Base
include Pundit
protect_from_forgery with: :null_session
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
protected
def authenticate_user_from_token!
100.times {print '|'}
p ''
p request.headers[:uid] # works
p request.headers[:authentication_token] # returns nil
p request.headers[:access_token] # returns nil
p request.headers['omfgwork'] # works
p request.headers[:omfgwork] # works
100.times {print '|'}
p ''
# return if request.headers[:uid].nil? or request.headers[:access_token].nil?
user_email = request.headers[:uid].presence
user = user_email && User.find_by_email(user_email)
# Notice how we use Devise.secure_compare to compare the token
# in the database with the token given in the params, mitigating
# timing attacks.
if user && Devise.secure_compare(user.authentication_token, request.headers[:access_token])
sign_in user, store: false
end
end
def user_not_authorized(exception)
policy_name = exception.policy.class.to_s.underscore
render json: "#{policy_name}: Not authorized", status: :forbidden
end
end
In articles controller:
before_action :set_article, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user_from_token!, except: [:index, :show]
before_action :authenticate_user!, except: [:index, :show]
Turns out, I wasn't able to access any headers which had an underscore in them directly, using the symbol or string representation. Upon iterating request.headers hash, I found that the headers sent in the request get prefixed with HTTP_ after getting capitalised. So in my case, the access_token header sent with the request can be accessed using request.headers[:HTTP_ACCESS_TOKEN] or request.headers['HTTP_ACCESS_TOKEN'] in the rails app.
However, if the header is in the form of a single word, like kites, it can be accessed directly using request.headers[:kites].
I am using devise to sign up/in.
routes
get 'profile' => 'profile#get_profile'
post 'profile' => 'profile#create_profile'
and profile_controller
def get_profile
render json: {user: current_user}, status: :ok
end
def create_profile
render json: {user: current_user}, status: :ok
end
GET: http://localhost:3000/user/profile returns the expected output. However,
POST request throws an error saying:
ActionController::InvalidAuthenticityToken in User::ProfileController#create_profile.
Please demystify this behavior.
To disable CSRF protection you can edit your ApplicationControllerlike this:
class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
# ...
end
or disable the CSRF protection for specific controller:
class ProfilesController < ApplicationController
skip_before_action :verify_authenticity_token
# ...
end
:null_session strategy empties the session instead of raising an exception which is perfect for an API. Because the session is empty, you can't use current_user method or othes helpers that refer to the session.
IMPORTANT:
protect_from_forgery with: :null_session must be used only in specific
cases, for example to allow API request (POST/PUT/PATCH/DELETE) without html form
With protect_from_forgery with: :null_session you must restrict access to your data with an authorization system because every one could do request against your API endpoint
Don't remove protect_from_forgery with: :exception for requests that are done through html form, is dangerous! (read here http://guides.rubyonrails.org/security.html#cross-site-request-forgery-csrf)
To handle both standard requests (through html form) and API requests generally you have to set up two different controller for the same resource. Example:
Routes
Rails.application.routes.draw do
resources :profiles
namespace :api do
namespace :v1 do
resources :profiles
end
end
end
ApplicationController
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
end
ProfilesController
(standard controller for html requests)
# app/controllers/profiles_controller.rb
class ProfilesController < ApplicationController
# POST yoursites.com/profiles
def create
end
end
Api::V1::ProfilesController
(controller for API requests)
# app/controllers/api/v1/profiles_controller.rb
module Api
module V1
class ProfilesController < ApplicationController
# To allow only json request
protect_from_forgery with: :null_session, if: Proc.new {|c| c.request.format.json? }
# POST yoursites.com/api/v1/profiles
def create
end
end
end
end
refereces:
http://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html#method-i-protect_from_forgery
Get requests don't have an authenticity token.
You will have to add the request forgery stuff to your forms using this
<%= csrf_meta_tag %>
And address via javascript
$('meta[name="csrf-token"]')
In ApplicationController (or another controller your controllers inherit from) there's a line:
protect_from_forgery with: :exception
Remove it and CSRF checks will be disabled.
I have a devise user ( that I call a Provider in my app) and I am trying to write a custom authentication method to prevent the Providers from deleting each other's posts (called Procedures in my app). Right now I have the correct_user method in my procedures controller.
def correct_user
#provider = #procedure.provider.find(params[:id])
redirect_to(provider_path(current_provider)) unless current_provider?(#provider)
end
I call it with the following before filter, also in my procedures controller:
before_filter :correct_user, :except => [:index, :show]
And I get the following error when trying to edit a procedure, even the provider's own procedure:
NoMethodError (undefined method `provider' for nil:NilClass)
app/controllers/procedures_controller.rb:8:in `correct_user'
Parameters: {"id"=>"523"}
From the looks of this error, the correct_user method is finding the procedure id instead of the provider id. How can I fix this? Thanks
Authentication is about making sure that the user is who he says is. Devise is an authorization library. The only access control it provides is that you can make actions off limits for unknown users.
Authorization is making rules about who gets to do what. Popular libraries include Pundit & CanCanCan.
Even without a lib you could write a simple authorization rule like this:
class Provider < ActiveRecord::Base
class NotAuthorized < StandardError; end
end
class ApplicationController < ActionController::Base
rescue_from Provider::NotAuthorized, with: :deny_access
private
def deny_access
render 'some_view', status: 403
end
end
class ProceduresController < ApplicationController
before_action :find_procedure, only: [:show, :edit, :update, :destroy]
before_action :authorize_resource!, except: [:new, :index, :show]
# DELETE /procedures/:id
def destroy
# This line never gets run if the user is not authorized.
#procedure.destroy
end
private
def find_procedure
#procedure = Procedure.find(params[:id])
end
def authorize_resource!
unless current_provider == #procedure.provider
raise Provider::NotAuthorized and return false
end
end
end
Notice that in the authorize_resource! method you compare the user id of the record that you are authorizing against the user id from the session.
If you used the id from the params you're leaving yourself wide open to a spoofing attack where a user pretends to be someone else by passing another user's id in the params.
However, I would not recommend that you write an authorization solution from scratch unless you really know what you are doing.
The error message tell you this:
Your variable #procedure is nil at the time that the method correct_user is called.
I have a simple before action which requires a user to be passed, though when authenticating I don't require it. Everything works locally, but when running the same tests (using postman) on the code that is up on heroku, I get the error associated with the :require_user method. I will write a simple example of what I'm trying to do below
before_action :require_user, except: [ :blah, :auth ]
def auth
# do something and render appropriate json
end
private
def require_user
#user = User.find_by(id: params[:id])
return true if #user
render json: { errors: 'Could not find user' }, status: 400
end
When I try to do the auth method on heroku, I get the Could not find user as the return value, which must mean it is hitting the require_user method...
And I figured it out... I was sending the post to blah.heroku.com instead of blah.herokuapp.com There is some redirect explained [1] which for some still unknown reason was messing up and running the before_action...
[1] https://devcenter.heroku.com/articles/error-codes#h16-redirect-to-herokuapp-com