Ist diese Rails JSON-Authentifizierungs-API (mit Devise) sicher?

70

Meine Rails-App verwendet Devise zur Authentifizierung. Es verfügt über eine Schwester-iOS-App, und Benutzer können sich mit denselben Anmeldeinformationen, die sie für die Web-App verwenden, bei der iOS-App anmelden. Ich brauche also eine Art API für die Authentifizierung.

Viele ähnliche Fragen hier verweisen auf dieses Tutorial , aber es scheint veraltet zu sein, da das token_authenticatableModul inzwischen aus Devise entfernt wurde und einige der Zeilen Fehler auslösen. (Ich verwende Devise 3.2.2.) Ich habe versucht, mein eigenes basierend auf diesem Tutorial (und diesem ) zu rollen , aber ich bin nicht 100% sicher - ich habe das Gefühl, dass es etwas gibt, das ich habe missverstanden oder verpasst.

Zunächst habe ich gemäß den Anweisungen dieses Kerns ein authentication_tokenTextattribut zu meiner usersTabelle hinzugefügt und Folgendes zu 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

Dann habe ich folgende Controller:

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

(Beachten Sie, dass mein application_controllerdie Linie hat before_filter :authenticate_user!.)

api / session_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

Und in config / route.rb :

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

Ich bin ein bisschen überfordert und ich bin mir sicher, dass es hier etwas gibt, auf das mein zukünftiges Ich zurückblicken und zusammenzucken wird (das gibt es normalerweise). Einige zweifelhafte Teile:

Erstens werden Sie feststellen, dass Api::SessionsControllererbt von, Devise::RegistrationsControllerwährend Api::RegistrationsControllererbt von ApiController(Ich habe auch einige andere Controller, z. B. Api::EventsController < ApiControllerdie sich mit mehr Standard-REST-Dingen für meine anderen Modelle befassen und nicht viel Kontakt mit Devise haben.) Dies ist eine ziemlich hässliche Anordnung. Aber ich konnte keinen anderen Weg finden, um auf die Methoden zuzugreifen, die ich brauche Api::RegistrationsController. Das Tutorial, auf das ich oben verlinkt habe, enthält die Zeile include Devise::Controllers::InternalHelpers, aber dieses Modul scheint in neueren Versionen von Devise entfernt worden zu sein.

Zweitens habe ich den CSRF-Schutz mit der Leitung deaktiviert skip_before_filter :verify_authentication_token. Ich habe meine Zweifel, ob dies eine gute Idee ist - ich sehe viele widersprüchliche oder schwer verständliche Ratschläge, ob JSON-APIs für CSRF-Angriffe anfällig sind -, aber das Hinzufügen dieser Zeile war der einzige Weg, um das verdammte Ding zum Laufen zu bringen.

Drittens möchte ich sicherstellen, dass ich verstehe, wie die Authentifizierung funktioniert, wenn sich ein Benutzer angemeldet hat. Angenommen, ich habe einen API-Aufruf, GET /api/friendsder eine Liste der Freunde des aktuellen Benutzers zurückgibt. Wie ich es verstehe, würde der iOS - App den Benutzer erhalten hat authentication_tokenaus der Datenbank (die ein fester Wert für jeden Benutzer, die ich nie ändern ??), legen sie dann als param zusammen mit jeder Anfrage, zum Beispiel GET /api/friends?authentication_token=abcdefgh1234, dann mein Api::FriendsControllertun könnte so etwas wie User.find_by(authentication_token: params[:authentication_token])den current_user zu bekommen. Ist es wirklich so einfach oder fehlt mir etwas?

Also für alle, die es geschafft haben, bis zum Ende dieser Mammutfrage zu lesen, danke für Ihre Zeit! Zusammenfassen:

  1. Ist dieses Anmeldesystem sicher? Oder gibt es etwas, das ich übersehen oder missverstanden habe, z. B. bei CSRF-Angriffen?
  2. Verstehe ich, wie Anforderungen authentifiziert werden, wenn Benutzer angemeldet sind? (Siehe "drittens ..." oben.)
  3. Gibt es eine Möglichkeit, diesen Code zu bereinigen oder zu verbessern? Besonders das hässliche Design, einen Controller von Devise::RegistrationsControllerund die anderen von erben zu lassen ApiController.

Vielen Dank!

GMA
quelle
Ihr Api::SessionsControllererstreckt sich von Devise::RegistrationsController..
Koen.

Antworten:

58

Sie möchten CSRF nicht deaktivieren. Ich habe gelesen, dass die Leute denken, dass es aus irgendeinem Grund nicht für JSON-APIs gilt, aber dies ist ein Missverständnis. Um es aktiviert zu halten, möchten Sie einige Änderungen vornehmen:

  • Fügen Sie auf der Serverseite Ihrem Sitzungscontroller einen after_filter hinzu:

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

    Dadurch wird ein Token generiert, in Ihre Sitzung eingefügt und für ausgewählte Aktionen in den Antwortheader kopiert.

  • Auf der Clientseite (iOS) müssen Sie sicherstellen, dass zwei Dinge vorhanden sind.

    • Ihr Client muss alle Serverantworten nach diesem Header durchsuchen und bei der Weitergabe beibehalten.

      ... 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];
      
    • Schließlich muss Ihr Client dieses Token zu allen von ihm gesendeten Nicht-GET-Anforderungen hinzufügen:

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

Das letzte Puzzleteil besteht darin, zu verstehen, dass beim Anmelden zum Entwickeln zwei aufeinanderfolgende Sitzungen / CSRF-Token verwendet werden. Ein Anmeldefluss würde folgendermaßen aussehen:

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
beno1604
quelle
Danke, das ist wirklich hilfreich. Habe ich also Recht, wenn ich denke, dass ich nach der auth_tokenAnmeldung die Antwort abrufen und diese zusammen mit den nachfolgenden Anforderungen zur Authentifizierung als dieser Benutzer weitergeben muss?
GMA
Dies erfolgt automatisch, wenn Sie die Art und Weise ändern, in der Ihr Client seine (Nicht-GET-) Anforderungen sendet, wie ich gezeigt habe. Übrigens setzt dies alles voraus, dass Sie die standardmäßige sitzungsbasierte Authentifizierung von devise verwenden. Wenn Sie sich mit Token authentifizieren, benötigen Sie einen anderen Anmeldefluss, da Sie sich über die Details dort nicht sicher sind.
Beno1604
1
Um dies zu verdeutlichen, passiert Folgendes? 1) Die iOS-App ruft an GET /users/sign_inund erhält das CSRF-Token. 2) Es POST /users/sign_inwird das gerade empfangene CSRF-Token verwendet und erhält ein neues CSRF-Token. Dadurch wird auch ein Cookie in der iOS-App gespeichert und eine neue Sitzung erstellt. 3) Für alle zukünftigen Anforderungen authentifiziert sich die iOS-App mithilfe des Cookies und enthält das CSRF-Token zum Schutz bei Nicht-GET-Anforderungen. Habe ich recht?
GMA
Ja, das ist es im Grunde. Zur Verdeutlichung werden Cookies / Sitzungen sowohl im anonymen als auch im angemeldeten Status verwendet.
Beno1604
1
Was ist, wenn Sie überhaupt keine Cookies / Sitzungen verwenden? Warum sollten Sie sich für CSRF interessieren als? Muss mich nur um XSS kümmern als.
wenn __name__ None ist
3

Ihr Beispiel scheint den Code aus dem Devise-Blog nachzuahmen - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

Wie in diesem Beitrag erwähnt, machen Sie es ähnlich wie Option 1, die als unsichere Option bezeichnet wird. Ich denke, der Schlüssel ist, dass Sie das Authentifizierungstoken nicht jedes Mal zurücksetzen möchten, wenn der Benutzer gespeichert wird. Ich denke, das Token sollte explizit erstellt werden (durch eine Art TokenController in der API) und sollte regelmäßig ablaufen.

Sie werden feststellen, dass ich "Ich denke" sage, da (soweit ich das beurteilen kann) niemand mehr Informationen dazu hat.

Jaco Pretorius
quelle
0

Die 10 häufigsten Schwachstellen in Webanwendungen sind in den OWASP Top 10 dokumentiert . In dieser Frage wurde erwähnt, dass der CSRF-Schutz (Cross-Site Request Forgery) deaktiviert wurde und CSRF unter den OWASDP Top 10 ist . Kurz gesagt, CSRF wird von Angreifern verwendet, um Aktionen als authentifizierter Benutzer auszuführen. Das Deaktivieren des CSRF-Schutzes führt zu Sicherheitslücken in einer Anwendung mit hohem Risiko und untergräbt den Zweck eines sicheren Authentifizierungssystems. Es ist wahrscheinlich, dass der CSRF-Schutz fehlgeschlagen ist, weil der Client das CSRF-Synchronisationstoken nicht übergeben kann.

Lesen Sie die gesamten OWASP Top 10, wenn Sie dies nicht tun, ist dies äußerst gefährlich . Achten Sie genau auf die fehlerhafte Authentifizierung und Sitzungsverwaltung. Lesen Sie auch das Spickzettel zur Sitzungsverwaltung .

Turm
quelle
9
Wie trifft dies auf eine RESTful-API zu? Eine RESTful-API verwendet keine Sitzungen! Ein Hacker müsste einen Javascript-Aufruf für die API verwenden, der auf den lokalen HTTP-Speicher oder Site-Cookies zugreifen müsste. Dies würde im Wesentlichen das Abrufen des schädlichen Skripts auf Ihrer Site erfordern. An diesem Punkt scheint es viele einfachere Möglichkeiten zu geben das System angreifen.
RonLugge