web-dev-qa-db-fra.com

Est-ce Rails API d'authentification JSON (en utilisant Devise) sécurisé?

Mon Rails utilise Devise pour l'authentification. Elle a une application iOS sœur, et les utilisateurs peuvent se connecter à l'application iOS en utilisant les mêmes informations d'identification qu'ils utilisent pour l'application Web. J'ai donc besoin d'une sorte de l'API pour l'authentification.

Beaucoup de questions similaires sur ici pointent vers ce tutoriel , mais il semble être obsolète, car le module token_authenticatable A depuis été supprimé de Devise et certaines des lignes renvoient les erreurs. (J'utilise Devise 3.2.2.) J'ai essayé de lancer le mien sur la base de ce tutoriel (et celui-ci ), mais je ne suis pas à 100% confiant - j'ai l'impression que il y a peut-être quelque chose que j'ai mal compris ou manqué.

Tout d'abord, suivant les conseils de this Gist , j'ai ajouté un attribut de texte authentication_token À ma table users, et le suivant à 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

Ensuite, j'ai les contrôleurs suivants:

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

(Notez que mon application_controller A la ligne 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

Et dans config/routes.rb :

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

Je suis un peu hors de ma profondeur et je suis sûr qu'il y a quelque chose ici que mon futur moi regardera en arrière et grincera des dents (il y en a généralement). Quelques parties douteuses:

Premièrement , vous remarquerez que Api::SessionsController Hérite de Devise::RegistrationsController Tandis que Api::RegistrationsController Hérite de ApiController (J'ai également d'autres contrôleurs tels que Api::EventsController < ApiController qui traitent des choses plus standard REST pour mes autres modèles et n'ont pas beaucoup de contacts avec Devise.) C'est un arrangement assez laid, mais je n'ai pas pu trouver un autre moyen d'accéder aux méthodes dont j'ai besoin dans Api::RegistrationsController. Le tutoriel que j'ai lié ci-dessus a la ligne include Devise::Controllers::InternalHelpers, Mais ce module semble avoir été supprimé dans les versions plus récentes de Devise.

Deuxièmement , j'ai désactivé la protection CSRF avec la ligne skip_before_filter :verify_authentication_token. J'ai des doutes quant à savoir si c'est une bonne idée - je vois beaucoup de conseils conflictuels ou difficiles à comprendre sur la vulnérabilité des API JSON aux attaques CSRF - mais en ajoutant que la ligne était le seul moyen de faire fonctionner cette fichue chose.

Troisièmement , je veux m'assurer de comprendre le fonctionnement de l'authentification une fois qu'un utilisateur s'est connecté. Disons que j'ai un appel API GET /api/friends Qui renvoie une liste des amis de l'utilisateur actuel. Si je comprends bien, l'application iOS devrait obtenir le authentication_token De l'utilisateur dans la base de données (qui est une valeur fixe pour chaque utilisateur qui ne change jamais ??), puis la soumettre en tant que paramètre avec chaque demande, par exemple GET /api/friends?authentication_token=abcdefgh1234, Alors mon Api::FriendsController Pourrait faire quelque chose comme User.find_by(authentication_token: params[:authentication_token]) pour obtenir l'utilisateur actuel. Est-ce vraiment aussi simple que ça, ou est-ce que je manque quelque chose?

Donc, pour tous ceux qui ont réussi à lire jusqu'à la fin de cette question gigantesque, merci pour votre temps! Pour résumer:

  1. Ce système de connexion est-il sécurisé? Ou y a-t-il quelque chose que j'ai oublié ou mal compris, par exemple en ce qui concerne les attaques CSRF?
  2. Ma compréhension de la façon d'authentifier les demandes une fois les utilisateurs connectés est-elle correcte? (Voir "troisième ..." ci-dessus.)
  3. Existe-t-il un moyen de nettoyer ou d'améliorer ce code? En particulier la conception laide d'avoir un contrôleur hérité de Devise::RegistrationsController Et le d'autres de ApiController.

Merci!

69
GMA

Vous ne voulez pas désactiver CSRF, j'ai lu que les gens pensent que cela ne s'applique pas aux API JSON pour une raison quelconque, mais c'est un malentendu. Pour le garder activé, vous souhaitez apporter quelques modifications:

  • côté serveur, ajoutez un after_filter à votre contrôleur de sessions:

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

    Cela générera un jeton, le placera dans votre session et le copiera dans l'en-tête de réponse pour les actions sélectionnées.

  • côté client (iOS), vous devez vous assurer que deux choses sont en place.

    • votre client doit analyser toutes les réponses du serveur pour cet en-tête et le conserver lorsqu'il est transmis.

      ... 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];
      
    • enfin, votre client doit ajouter ce jeton à toutes les demandes "non GET" qu'il envoie:

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

La dernière pièce du puzzle est de comprendre que lors de la connexion pour concevoir, deux jetons sessions/csrf suivants sont utilisés. Un flux de connexion ressemblerait à ceci:

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
57
beno1604

Votre exemple semble imiter le code du blog Devise - https://Gist.github.com/josevalim/fb706b1e933ef01e4fb6

Comme mentionné dans ce post, vous le faites de manière similaire à l'option 1, qui, selon eux, est l'option non sécurisée. Je pense que la clé est que vous ne voulez pas simplement réinitialiser le jeton d'authentification chaque fois que l'utilisateur est enregistré. Je pense que le jeton devrait être créé explicitement (par une sorte de TokenController dans l'API) et devrait expirer périodiquement.

Vous remarquerez que je dis "je pense" car (pour autant que je sache), personne n'a plus d'informations à ce sujet.

3
Jaco Pretorius

Les 10 vulnérabilités les plus courantes dans les applications Web sont documentées dans le OWASP Top 1 . Cette question mentionnait que la protection contre la contrefaçon de requêtes intersites (CSRF) était désactivée et CSRF figure dans le Top 10 OWASDP . En bref, CSRF est utilisé par les attaquants pour effectuer des actions en tant qu'utilisateur authentifié. La désactivation de la protection CSRF entraînera des vulnérabilités à haut risque dans une application et sapera l'objectif d'avoir un système d'authentification sécurisé. Il est probable que la protection CSRF échoue, car le client ne parvient pas à transmettre le jeton de synchronisation CSRF.

Lisez l'intégralité du top 10 de l'OWASP, à défaut de le faire extrêmement dangereux . Portez une attention particulière à Authentification brisée et gestion de session , consultez également le Aide-mémoire de gestion de session .

0
rook