My Rails uses the enhancement feature for authentication. He has an iOS sister app, and users can log in to the iOS app using the same credentials that they use for the web application. So I need some kind of API for authentication.
Many similar questions point to this tutorial here , but it seems deprecated since the token_authenticatable module has since been removed from Devise, and some of the lines throw errors. (I am using Devise 3.2.2.) I tried to overturn my own based on this tutorial (and this one ), but I'm not 100% sure about it - I feel that there might be something that I misunderstood or missed.
Firstly, following the recommendations of this essence , I added the authentication_token text attribute to the users table and the following before user.rb :
before_save :ensure_authentication_token def ensure_authentication_token if authentication_token.blank? self.authentication_token = generate_authentication_token end end private def generate_authentication_token loop do token = Devise.friendly_token break token unless User.find_by(authentication_token: token) end end
Then I have the following controllers:
api_controller.rb
class ApiController < ApplicationController respond_to :json skip_before_filter :authenticate_user! protected def user_params params[:user].permit(:email, :password, :password_confirmation) end end
(Note that my application_controller has a line before_filter :authenticate_user! )
api / sessions _controller.rb
class Api::SessionsController < Devise::RegistrationsController prepend_before_filter :require_no_authentication, :only => [:create ] before_filter :ensure_params_exist respond_to :json skip_before_filter :verify_authenticity_token def create build_resource resource = User.find_for_database_authentication( email: params[:user][:email] ) return invalid_login_attempt unless resource if resource.valid_password?(params[:user][:password]) sign_in("user", resource) render json: { success: true, auth_token: resource.authentication_token, email: resource.email } return end invalid_login_attempt end def destroy sign_out(resource_name) end protected def ensure_params_exist return unless params[:user].blank? render json: { success: false, message: "missing user parameter" }, status: 422 end def invalid_login_attempt warden.custom_failure! render json: { success: false, message: "Error with your login or password" }, status: 401 end end
api / registrations _controller.rb
class Api::RegistrationsController < ApiController skip_before_filter :verify_authenticity_token def create user = User.new(user_params) if user.save render( json: Jbuilder.encode do |j| j.success true j.email user.email j.auth_token user.authentication_token end, status: 201 ) return else warden.custom_failure! render json: user.errors, status: 422 end end end
And in config / routes.rb :
namespace :api, defaults: { format: "json" } do devise_for :users end
I am a little from the depths, and I am sure that there is something here that my future will look back and shrink (usually there is). Some parts of iffy:
First, you will notice that Api::SessionsController inherits from Devise::RegistrationsController , while Api::RegistrationsController inherits from ApiController (I also have some other controllers like Api::EventsController < ApiController that deal with a lot of standard REST stuff for my other models and doesn't have much contact with Devise.) This is a pretty ugly convention, but I couldn't find another way to access the methods that I need in Api::RegistrationsController . The tutorial linked above has the line include Devise::Controllers::InternalHelpers , but this module seems to have been removed in later versions of Devise.
Secondly, I turned off CSRF protection using the skip_before_filter :verify_authentication_token . I have doubts as to whether this is a good idea - I see a lot of conflicting or hard to understand tips about whether the JSON APIs are vulnerable to CSRF attacks, but adding that this line was the only way I could get this damn job.
Thirdly, I want to make sure that I understand how authentication works after user login. Let's say I have a GET /api/friends API call that returns a list of the user's current friends. As I understand it, an iOS application would have to get the authentication_token user from the database (which is a fixed value for each user that never changes?), And then send it as a parameter along with each request, for example. GET /api/friends?authentication_token=abcdefgh1234 , then my Api::FriendsController could do something like User.find_by(authentication_token: params[:authentication_token]) to get current_user. Is it really that simple, or am I missing something?
So for those who managed to read all the way to the end of this mammoth question, thanks for your time! Summarizing:
- Is this login system safe? Or is there something that I missed or misunderstood, for example. when it comes to CSRF attacks?
- Is my understanding of how to authenticate requests when users are signed up correctly? (see "third ..." above.)
- Can this code be cleaned or improved? In particular, the ugly design is that one controller inherits from
Devise::RegistrationsController , and the rest from ApiController .
Thank!