Creating JSON API inside existing Rails app - ruby-on-rails

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.

Related

Rails - How to use Devise in both API and non-API mode in the same application

rails 7.0.3.1
devise 4.8.1
I'm aware that its possible to use Devise in API mode. But is it possible to use Devise both in API and non-API mode, "at the same time"? One use case of this is having one Rails app both serving its own static pages and being a back-end to a front-end client (e.g. developed using React).
Not an expert, but i'm sure you will get other answers that will give more detailed answers. I think you can acheive what you want, with defining extra routes and extra set up.
I would ask myself is there any benefit of having rails views AND a seperate react app? Cant the same result be achieved by using only react frontend and a rails api?
There are some good tutorials by Deanin on YouTube describing a lot of react integration with rails. I have a rails app that has rails views which expose an Api to a React app, and this works well but the authentications are handled seperately for each app as I dont need it to be the same app doing this.
I have just built an application like this and your key challenge is Devise uses a cookie to authenticate in Rails static web pages but needs to use a token or JWT to authenticate on the API. Getting it to do both in 1 app can be a bit of a challenge. Here is how I solved it, very open to other / better ideas as the below works but I feel it's a little hacky:
In routes I have this (simplified):
devise_for :users
namespace :api, defaults: { format: :json } do
namespace :v1 do
devise_scope :user do
post "sign_up", to: "registrations#create"
post "sign_in", to: "sessions#create"
post "update/:id", to: "users#update"
end
end
end
root to: "pages#home"
This allows me to have both the default devise controller in the rails static pages and a devise mode for the API too.
In my application controller I am using authorising user for all actions and using pundit white list approach like this, it doesn't have to be this way, could be set up any number of ways:
class ApplicationController < ActionController::Base
before_action :authenticate_user!
include Pundit
before_action :configure_permitted_parameters, if: :devise_controller?
# Pundit: white-list approach.
after_action :verify_authorized, except: :index, unless: :skip_pundit?
protected
def skip_pundit?
devise_controller? || params[:controller] =~ /(^(rails_)?admin)|(^pages$)/
end
end
The above + standard login views generated by devise on rails generate devise:install cover my static page needs.
Then in the API there are some challenges.
I am using simple_token_auth gem - there are specific reasons for this and the particular app has a very low value to an account but I would probably recommend using a JWT either way: sessions and registrations work fine handled by their specific controllers which inherit from devise:
class Api::V1::RegistrationsController < Devise::RegistrationsController
skip_before_action :verify_authenticity_token
# sign up
def create
user = User.new user_params
if user.save
render json: {
messages: "Sign Up Successfully",
is_success: true,
data: { user: user }
}, status: :ok
else
render json: {
messages: "Sign Up Failed",
is_success: false,
data: {}
}, status: :unprocessable_entity
end
end
private
def user_params
params.require(:user).permit(:email, :name, etc.)
end
end
sessions:
class Api::V1::SessionsController < Devise::SessionsController
before_action :sign_in_params, only: :create
before_action :load_user, only: :create
skip_before_action :verify_authenticity_token, :only => :create
# sign in
def create
if #user.valid_password?(sign_in_params[:password])
sign_in "user", #user
render json: {
messages: "Signed In Successfully",
is_success: true,
data: {user: #user}
}, status: :ok
else
render json: {
messages: "Signed In Failed - Unauthorized",
is_success: false,
data: {}
}, status: :unauthorized
end
end
private
def sign_in_params
params.require(:sign_in).permit :email, :password
end
def load_user
#user = User.find_for_database_authentication(email: sign_in_params[:email])
if #user
return #user
else
render json: {
messages: "Cannot get User",
is_success: false,
data: {}
}, status: :failure
end
end
end
The challenge I found was with editing. I had to create a specific controller that did not inherit from devise but from the api V1 base_controller (which itself inherits from ActionController::API) to do this, it ended up looking like:
class Api::V1::UsersController < Api::V1::BaseController
acts_as_token_authentication_handler_for User
def update
user = User.find(params[:id])
if user.valid_password?(params[:current_password].to_s)
puts 'password matched'
user.update(user_params)
render json: {
messages: "Updated Successfully",
is_success: true,
data: {user: user}
}, status: 201
else
puts 'pw didnt match'
render json: {
messages: "Incorrect Password Please Try Again",
is_success: false,
data: {user: user}
}, status: 403
end
end
private
def user_params
params.require(:user).permit(:email, etc.)
end
end
This works but the security on the update path is questionable due to the simple_user_auth tokens being what they are. I've mitigated this in a few places but don't recommend copy/pasting for a production application. Use JWTs.
On the user model you also need the following:
acts_as_token_authenticatable
devise :database_authenticatable,
:recoverable, :rememberable, :validatable
I am not sure why inheriting from devise wouldn't work for the update path, but I spent 2 days on it before giving up and writing my own.

Devise Token Auth Will Not Authenticate for PUT Route

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

Heroku before_action :require_user, except: :auth being run for auth

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

UnknownFormat in Devise::SessionsController#new

I have a Rails 4 app (that was upgraded from Rails 3) in which I decided to delete one of the controllers. I then moved the methods from that deleted controller to the ApplicationController, which included before_filter :authenticate_user!
Here's what my ApplicationController looks like now:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :authenticate_user!
respond_to :json
def index
gon.rabl
#user = current_user
gon.rabl "app/views/users/show.json.rabl", as: "current_user"
end
def markdown
require 'redcarpet'
renderer = Redcarpet::Render::HTML.new
extensions = {}
Redcarpet::Markdown.new(renderer, extensions)
end
helper_method :markdown
end
Now, I'm getting this error:
ActionController::UnknownFormat in Devise::SessionsController#new
I think this might be due to the fact that you have set your application controller to respond only to json. If your Devise Controller inherits from ApplicationController (I think this is the default), then it will expect to see a content-type: json header, or your urls must all end in .json
You shouldn't have the index method defined in application_controller. You should move it to the appropriate controller. If this is something you want to do before every action you might want to try something like this:
before_action :gon_user, only: :index
private
def gon_user
gon.rabl
#user = current_user
gon.rabl "app/views/users/show.json.rabl", as: "current_user"
end
Though i've to be honest that i'm not sure about the gon stuff, can't remember if it was for moving data from ruby to javascript or for responding to ajax/json request.
Thanks Slicedpan. Got me thinking about a
respond_to :json
Used in my Rails Application as an API with Angular. As in my Rails controllers I use for requests from my Angular Services.
respond_with
In my case I ended up adding html to the respond_to:
respond_to :json, :html
The Default Mime Types can be seen here:
http://apidock.com/rails/Mime

Rails form_authenticity_token not regenerating after POST request

I was under the impression that Rails will regenerate the form_authenticity_token after any POST, PUT, or DELETE action. But for some reason, after a successful POST to the users resource the form_authenticity_token does not regenerate. I'm free to POST as many time as I would like with the same CSRF token over and over.
I've got a namespaced API and I'm using the RABL gem to build out my responses. This is how I have everything setup...
class Api::V1::UsersController < Api::V1::ApplicationController
...
def create
#user = User.new(params[:user])
render "show", :status => (#user.save ? :ok : :unprocessable_entity)
end
...
end
class Api::V1::ApplicationController < ApplicationController
layout '/api/v1/layouts/application.json.erb'
respond_to :json
before_filter :authenticate_user!
...
end
class ApplicationController < ActionController::Base
protect_from_forgery
end
The post goes through fine, there are no errors or warning in the development.log or in the consoled $ rails s log.
I've check verified_request? from within the create method and it's returning true. I've removed the render and setup a create.json.rabl view with the same code as the show.json.rabl view... no dice.
I'm running Rails 3.1.3 w/ Ruby 1.9.2p290 w/ a cookie session store.
The authenticity token is being sent via request header (X-CSRF-Token)
You were under the wrong impressions. As you can see from the relevant bit of the rails source the form authenticity token stays the same for the lifetime of the session.

Resources