Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

My Rails app uses Devise for authentication. It has a sister iOS app, and users can log in to the iOS app using the same credentials that they use for the web app. So I need some kind of API for authentication.

Lots of similar questions on here point to this tutorial, but it seems to be out-of-date, as the token_authenticatable module has since been removed from Devise and some of the lines throw errors. (I'm using Devise 3.2.2.) I've attempted to roll my own based on that tutorial (and this one), but I'm not 100% confident in it - I feel like there may be something I've misunderstood or missed.

Firstly, following the advice of this gist, I added an authentication_token text attribute to my users table, and the following to 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 the 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'm out of my depth a bit and I'm sure there's something here that my future self will look back on and cringe (there usually is). Some iffy parts:

Firstly, you'll notice that Api::SessionsController inherits from Devise::RegistrationsController whereas Api::RegistrationsController inherits from ApiController (I also have some other controllers such as Api::EventsController < ApiController which deal with more standard REST stuff for my other models and don't have much contact with Devise.) This is a pretty ugly arrangement, but I couldn't figure out another way of getting access the methods I need in Api::RegistrationsController. The tutorial I linked to above has the line include Devise::Controllers::InternalHelpers, but this module seems to have been removed in more recent versions of Devise.

Secondly, I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token. I have my doubts about whether this is a good idea - I see a lot of conflicting or hard to understand advice about whether JSON APIs are vulnerable to CSRF attacks - but adding that line was the only way I could get the damn thing to work.

Thirdly, I want to make sure I understand how authentication works once a user has signed in. Say I have an API call GET /api/friends which returns a list of the current user's friends. As I understand it, the iOS app would have to get the user's authentication_token from the database (which is a fixed value for each user that never changes??), then submit it as a param along with every request, e.g. GET /api/friends?authentication_token=abcdefgh1234, then my Api::FriendsController could do something like User.find_by(authentication_token: params[:authentication_token]) to get the current_user. Is it really this simple, or am I missing something?

So for anyone who's managed to read all the way to the end of this mammoth question, thanks for your time! To summarise:

  1. Is this login system secure? Or is there something I've overlooked or misunderstood, e.g. when it comes to CSRF attacks?
  2. Is my understanding of how to authenticate requests once users are signed in correct? (See "thirdly..." above.)
  3. Is there any way this code can be cleaned up or made nicer? Particularly the ugly design of having one controller inherit from Devise::RegistrationsController and the others from ApiController.

Thanks!

like image 865
GMA Avatar asked Dec 23 '13 14:12

GMA


People also ask

How does API authentication work in Rails?

The token-based verification method works simply. The user enters his details and sends the request to the server. If the information is correct, the server creates a unique HMACSHA256 encoded token, also known as the JSON (JWT) web token.


1 Answers

You don't want to disable CSRF, I have read that people think it doesn't apply to JSON APIs for some reason, but this is a misunderstanding. To keep it enabled, you want to make a few changes:

  • on there server side add a after_filter to your sessions 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 generate a token, put it in your session and copy it in the response header for selected actions.

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

    • your client needs to scan all server responses for this header and retain it when it is passed along.

      ... 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 out:

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

Final piece of the puzzle is to understand that when logging in to devise, two subsequent sessions/csrf tokens are being used. A login flow would 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 
like image 72
beno1604 Avatar answered Oct 14 '22 01:10

beno1604