Is this Rails JSON authentication API (using Devise) safe?

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!

+63
json security api ruby-on-rails devise
Dec 23 '13 at 14:48
source share
3 answers

You do not want to disable CSRF, I read that for some reason people do not belong to the JSON APIs, but this is a misunderstanding. To enable it, you want to make a few changes:

  • on the server side add after_filter to the session controller:

     after_filter :set_csrf_header, only: [:new, :create] protected def set_csrf_header response.headers['X-CSRF-Token'] = form_authenticity_token end 

    This will create a token, place it in your session and copy it into the response header for the selected actions.

  • client side (iOS) you need to make sure that two things are in place.

    • Your client needs to scan all server responses for this header and save it when it is sent.

       ... get ahold of response object // response may be a NSURLResponse object, so convert: NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse*)response; // grab token if present, make sure you have a config object to store it in NSString *token = [[httpResponse allHeaderFields] objectForKey:@"X-CSRF-Token"]; if (token) [yourConfig setCsrfToken:token]; 
    • finally, your client needs to add this token to all non-GET requests it sends:

       ... get ahold of your request object if (yourConfig.csrfToken && ![request.httpMethod isEqualToString:@"GET"]) [request setValue:yourConfig.csrfToken forHTTPHeaderField:@"X-CSRF-Token"]; 

The final part of the puzzle is to understand that when you enter the system, two subsequent sessions / csrf tokens are used. The input stream will look like this:

 GET /users/sign_in -> // new action is called, initial token is set // now send login form on callback: POST /users/sign_in <username, password> -> // create action called, token is reset // when login is successful, session and token are replaced // and you can send authenticated requests 
+53
Dec 29 '13 at 11:46
source share

Your example seems to mimic the code from the Devise blog - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

As mentioned in this article, you do this like option 1, which they say is unsafe. I think the key is that you do not want to simply reset the authentication token every time the user is saved. I think that the token should be created explicitly (by some TokenController in the API) and should expire periodically.

You will notice that I say “I think,” because (as far as I can tell) no one else knows about it.

+2
Jan 10 '14 at 1:46
source share
0
Dec 29 '13 at 6:31
source share



All Articles