¿Es segura esta API de autenticación JSON de Rails (usando Devise)?


Mi aplicación Rails utiliza Devise para la autenticación. Tiene una aplicación hermana de iOS, y los usuarios pueden iniciar sesión en la aplicación de iOS usando las mismas credenciales que usan para la aplicación web. Así que necesito algún tipo de API para la autenticación.

Muchas preguntas similares aquí apuntan a este tutorial, pero parece estar desactualizado, ya que el módulo token_authenticatable ha sido eliminado de Devise y algunas de las líneas arrojan errores. (Estoy usando Devise 3.2.2.) He intentado rodar mi propio basado en eso tutorial (y este), pero no estoy 100% seguro en él - Siento que puede haber algo que he malinterpretado o perdido.

En primer lugar, siguiendo el consejo de este gist, agregué un atributo de texto authentication_token a mi tabla users, y lo siguiente a 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

Entonces tengo los siguientes controladores:

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

(Tenga en cuenta que mi application_controller tiene la línea 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

Y en config/routes.rb :

  namespace :api, defaults: { format: "json" } do
    devise_for :users
  end

Estoy fuera de mi profundidad un poco y estoy seguro de que hay algo aquí que mi yo futuro mirará hacia atrás y se encogerá (generalmente lo hay). Algunas partes dudosas:

En primer lugar , notarás que Api::SessionsController hereda de Devise::RegistrationsController mientras que Api::RegistrationsController hereda de ApiController (también tengo algunos otros controladores como Api::EventsController < ApiController que se ocupan de cosas REST más estándar para mis otros modelos y no tienen mucho contacto con Devise.) Este es un arreglo bastante feo, pero no pude encontrar otra manera de obtener acceso a los métodos que necesito en Api::RegistrationsController. El tutorial al que enlacé anteriormente tiene la línea include Devise::Controllers::InternalHelpers, pero este módulo parece haber sido eliminado en versiones más recientes de Devise.

En segundo lugar , he desactivado la protección CSRF con la línea skip_before_filter :verify_authentication_token. Tengo mis dudas sobre si esta es una buena idea-Veo un montón de conflictivos o difíciles de entender consejos sobre si las API JSON son vulnerables a los ataques CSRF - pero agregar esa línea fue la única manera en que pude hacer que la maldita cosa funcionara.

En tercer lugar , quiero asegurarme de que entiendo cómo funciona la autenticación una vez que un usuario ha iniciado sesión. Digamos que tengo una llamada API GET /api/friends que devuelve una lista de los amigos del usuario actual. Según lo entiendo, la aplicación iOS tendría que obtener el authentication_token de la base de datos (¿cuál es un valor fijo para cada usuario que nunca cambia??), luego enviarlo como un param junto con cada solicitud, por ejemplo GET /api/friends?authentication_token=abcdefgh1234, entonces mi Api::FriendsController podría hacer algo como User.find_by(authentication_token: params[:authentication_token]) para obtener el current_user. Es realmente así de simple, o me estoy perdiendo algo?

Así que para cualquiera que haya logrado leer todo el camino hasta el final de esta pregunta gigantesca, ¡gracias por su tiempo! Para resumir:

  1. ¿Es seguro este sistema de inicio de sesión? O hay algo que he pasado por alto o mal entendido, por ejemplo, cuando se trata de ataques CSRF?
  2. ¿Entiendo correctamente cómo autenticar las solicitudes una vez que los usuarios han iniciado sesión? (Ver "en tercer lugar..." arriba.)
  3. ¿Hay alguna manera de que este código se pueda limpiar o hacer más bonito? Particularmente el feo diseño de tener un controlador heredado de Devise::RegistrationsController y los otros de ApiController.

Gracias!

Author: Community, 2013-12-23

3 answers

No desea deshabilitar CSRF, he leído que la gente piensa que no se aplica a las API JSON por alguna razón, pero esto es un malentendido. Para mantenerlo habilitado, desea realizar algunos cambios:

  • En el lado del servidor agregue un after_filter a su controlador de sesiones:

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

    Esto generará un token, lo pondrá en su sesión y lo copiará en el encabezado de respuesta para las acciones seleccionadas.

  • Lado del cliente (iOS) es necesario asegurarse de que dos cosas son en su lugar.

    • Su cliente necesita escanear todas las respuestas del servidor para este encabezado y conservarlo cuando se pase.

      ... 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];
      
    • Finalmente, su cliente necesita agregar este token a todas las solicitudes 'no GET' que envía:

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

La pieza final del rompecabezas es entender que al iniciar sesión en devise, se están utilizando dos tokens sessions/csrf posteriores. Un flujo de inicio de sesión se vería así:

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
 54
Author: beno1604,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-12-29 18:38:05

Tu ejemplo parece imitar el código del blog de Devise - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

Como se mencionó en ese post, lo estás haciendo de manera similar a la opción 1, que dicen que es la opción insegura. Creo que la clave es que no desea simplemente restablecer el token de autenticación cada vez que se guarda el usuario. Creo que el token debe ser creado explícitamente (por algún tipo de TokenController en la API) y debe expirar periódica.

Te darás cuenta de que digo 'creo' ya que (por lo que puedo decir) nadie tiene más información sobre esto.

 2
Author: Jaco Pretorius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-01-10 01:46:33

Las 10 vulnerabilidades más comunes en aplicaciones web están documentadas en el OWASP Top 10. Esta pregunta mencionó que la protección contra la Falsificación de Solicitudes entre sitios(CSRF) estaba deshabilitada, y CSRF está en el Top 10 de OWASDP. En resumen, CSRF es utilizado por los atacantes para realizar acciones como un usuario autenticado. Deshabilitar la protección CSRF conducirá a vulnerabilidades de alto riesgo en una aplicación y socavará el propósito de tener un sistema de autenticación seguro. Es probable que el La protección CSRF estaba fallando, porque el cliente no pasa el token de sincronización CSRF.

Leer el top 10 completo de OWASP, no hacerlo es extremadamente peligroso. Preste mucha atención a Autenticación rota y Administración de sesiones, también consulte la Hoja de trucos de Administración de sesiones .

 0
Author: rook,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-04-28 01:45:36