I am working on a project that is divided in two apps : - one rails JSON API that is dealing with the database and is rendering data as JSON - one "front-end" rails app that is sending requests to the API whenever it needs and displaying the json data in a nice way.
Authentification for the API is token based using gem'simple_token_authentication'
meaning that for most of the requests that are sent to the API you have to send the user token & his email in the header for the request to be authorized.
The one who worked on the project before me had also installed Devise authentification system on the API side to allow direct access to the API methods from the navigator after successfull login with email & password.
I just started coding on the "front-end app" that is supposed to request the API and I am having trouble especially with the authentification system.
As Devise was already installed on the API, I thought it would be a good idea to make the user login on the front-end app which would then request devise's methods present on the API for creating user, auth, reseting password...
The problem is that devise's methods are rendering html and not JSON so I actually had to override most of devise's controller. To give you a quick idea of how it works : You fill the sign up form on the front-end app then the params are sent to the front-end app controller that is then requesting devise's register user method on the API :
1) front-end app controller :
def create
# Post on API to create USER
@response = HTTParty.post(ENV['API_ADDRESS']+'users',
:body => { :password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:email => params[:user][:email]
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
# si le User est bien crée je récupère son email et son token, je les store en session et je redirige vers Account#new
if user_id = @response["id"]
session[:user_email] = @response["email"]
session[:user_token] = @response["authentication_token"]
redirect_to new_account_path
else
puts @response
@errors = @response["errors"]
puts @errors
render :new
end
end
2) API overrided devise controller :
class RegistrationsController < Devise::RegistrationsController
def new
super
end
def create
@user = User.new(user_params)
if @user.save
render :json => @user
else
render_error
end
end
def update
super
end
private
def user_params
params.require(:registration).permit(:password, :email)
end
def render_error
render json: { errors: @user.errors.full_messages }, status: :unprocessable_entity
end
end
This works ok. Here I send back the user that was just created on the API as JSON and I store is auth token and his email in the session hash.
My problem is with the reset_password method for which I am trying to reuse some of devise code. First, I ask for a reset of the password which generates a reset password token for the user who requested the change. This also generates an email to the user with a link (with the token inside) pointing to the reset password form for the specific user. This is working well. I am getting the link in the email then going to the edit_password form on my front-end app :
<form action="/users/password" method='post'>
<input name="authenticity_token" value="<%= form_authenticity_token %>" type="hidden">
<%= hidden_field_tag "[user][reset_password_token]", params[:reset_password_token] %>
<%=label_tag "Password" %>
<input type="text" name="[user][password">
<%=label_tag "Password Confirmation" %>
<input type="text" name="[user][password_confirmation]">
<input type="Submit" value="change my password">
</form>
When the form is submitted it goes through my front-end app controller :
def update_password
@response = HTTParty.patch(ENV['API_ADDRESS']+'users/password',
:body => {
:user => {
:password => params[:user][:password],
:password_confirmation => params[:user][:password_confirmation],
:reset_password_token => params[:user][:reset_password_token]
}
}.to_json,
:headers => { 'Content-Type' => 'application/json' })
end
which then calls my overrided Devise::PasswordController (update method) :
# app/controllers/registrations_controller.rb
class PasswordsController < Devise::RegistrationsController
# POST /resource/password
def create
if resource_params[:email].blank?
render_error_empty_field and return
end
self.resource = resource_class.send_reset_password_instructions(resource_params)
yield resource if block_given?
if successfully_sent?(resource)
render_success
else
render_error
end
end
def update
self.resource = resource_class.reset_password_by_token(resource_params)
yield resource if block_given?
if resource.errors.empty?
resource.unlock_access! if unlockable?(resource)
render_success
else
render_error
end
end
private
# TODO change just one big method render_error with different cases
def render_success
render json: { success: "You will receive an email with instructions on how to reset your password in a few minutes." }
end
def render_error
render json: { error: "Ce compte n'existe pas." }
end
def render_error_empty_field
render json: { error: "Merci d'entrer un email" }
end
end
However the request is always Unauthorized :
Started PATCH "/users/password" for ::1 at 2016-02-05 11:28:30 +0100
Processing by PasswordsController#update as HTML
Parameters: {"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}, "password"=>{"user"=>{"password"=>"[FILTERED]", "password_confirmation"=>"[FILTERED]", "reset_password_token"=>"[FILTERED]"}}}
Completed 401 Unauthorized in 1ms (ActiveRecord: 0.0ms)
I dont understand why is this last request unauthorized ?
Your predecessor likely made a mess of things on the API side just for his convenience.
We know that using cookies for API's is a really bad idea since it leaves the doors wide open for CSRF/XSRF attacks.
We can't use the Rails CSRF protection for an API because it only works as sort of guarantee that the request originated from our own server. And an API that can only be used from your own server is not very useful.
Devise by default uses a cookie based auth strategy because thats what works for web based applications and Devise is all about making auth in web based applications easy.
So what you should do is either remove Devise completely from the API app or convert Devise to use a token based strategy. You also should consider removing the sessions middleware from the API app. Also the Devise controllers are so heavily slanted towards client interaction so that trying to beat them into API controllers is going to be very messy.
Updating a password in an API is just:
class API::V1::Users::PasswordsController
before_action :authenticate_user!
def create
@user = User.find(params[:user_id])
raise AccessDenied unless @user == current_user
@user.update(password: params[:password])
respond_with(@user)
end
end
This is a very simplified example - but the point is if you strip off all the junk from the controller related to forms / flashes and redirects there is not that much you are really going to re-use.
If your front-end app is a "classical" client/server Rails app then you can use a regular cookie based auth (Devise) and let it share the database with the API app. Token based auth does not work well with classical client/server apps due to its stateless nature.
If the front end app is a SPA like Angular or Ember.js you might want to look into setting up your own OAuth provider with Doorkeeper instead.