Czy to API uwierzytelniania Rails JSON (za pomocą Devise) jest bezpieczne?

Moja aplikacja Rails używa Devise do uwierzytelniania. Ma siostrzaną aplikację na iOS, a użytkownicy mogą logować się do aplikacji na iOS za pomocą tych samych poświadczeń, których używają w aplikacji internetowej. Więc potrzebuję jakiegoś API do uwierzytelniania.

Wiele podobnych pytań tutaj wskazuje na ten tutorial, ale wydaje się być nieaktualny, ponieważ moduł token_authenticatable został usunięty z Devise i niektóre linie rzucają błędy. (Używam Devise 3.2.2.) Starałem się w oparciu o to tutorial (i ten ), ale nie jestem w tym 100% pewien-czuję, że może być coś, co źle zrozumiałam lub przegapiłam.

Po pierwsze, za radą tego gist , dodałem authentication_token atrybut tekstowy do mojej tabeli users, a następnie do 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

Następnie mam następujące Kontrolery:

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

(zauważ, że mój application_controller ma linię 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

Oraz w config / routes.rb :

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

Jestem trochę poza moją głębią i jestem pewien, że jest tu coś, na co moje przyszłe ja będzie patrzeć wstecz i marudzić (zwykle jest). Niektóre iffy części:

Po pierwsze , zauważysz, że Api::SessionsController dziedziczy z Devise::RegistrationsController, podczas gdy Api::RegistrationsController dziedziczy z ApiController (mam też inne Kontrolery takie jak Api::EventsController < ApiController, które zajmują się bardziej standardowymi rzeczami odpoczynku dla innych moich modeli i nie mają zbyt dużego kontaktu z Devise.) Jest to dość brzydki układ, ale nie mogłem wymyślić innego sposobu uzyskania dostępu do metod, których potrzebuję w Api::RegistrationsController. Tutorial, do którego podlinkowałem powyżej, ma linię include Devise::Controllers::InternalHelpers, ale ten moduł został usunięty w nowszych wersjach Devise.

Po Drugie, wyłączyłem ochronę CSRF za pomocą linii skip_before_filter :verify_authentication_token. Mam wątpliwości, czy to dobry pomysł - widzę wiele sprzecznych lub trudnych do zrozumienia porad na temat tego, czy interfejsy API JSON są podatne na ataki CSRF - ale dodanie tej linii było jedynym sposobem, aby to cholerstwo zadziałało.

Po Trzecie , chcę się upewnić, że rozumiem, jak działa uwierzytelnianie po zalogowaniu się użytkownika. Powiedzmy, że mam wywołanie API GET /api/friends, które zwraca listę znajomych bieżącego użytkownika. Jak rozumiem, aplikacja na iOS musiałaby uzyskać użytkownika authentication_token z bazy danych (która jest stałą wartością dla każdego użytkownika, który nigdy się nie zmienia??GET /api/friends?authentication_token=abcdefgh1234, wtedy mój Api::FriendsController mógłby zrobić coś takiego jak User.find_by(authentication_token: params[:authentication_token]), aby uzyskać current_user. Czy to naprawdę takie proste, czy coś mi umyka?

Więc dla wszystkich, którzy zdołali przeczytać całą drogę do końca tego mamutowego pytania, dziękuję za poświęcony czas! Podsumowując:

  1. czy ten system logowania jest bezpieczny? czy jest coś, co mam pomijane lub niezrozumiane, np. jeśli chodzi o ataki CSRF?
  2. czy moje zrozumienie, jak uwierzytelniać żądania po zalogowaniu użytkowników, jest poprawne? (Patrz " po trzecie..."powyżej.)
  3. czy jest jakiś sposób, aby ten kod mógł być oczyszczony lub ładniejszy? szczególnie brzydka konstrukcja posiadania jednego kontrolera dziedziczy z Devise::RegistrationsController, A innych z ApiController.
Dzięki!
Author: Community, 2013-12-23

3 answers

Nie chcesz wyłączać CSRF, czytałem, że ludzie myślą, że nie dotyczy to API JSON z jakiegoś powodu, ale jest to nieporozumienie. Aby go włączyć, należy wprowadzić kilka zmian:

  • Po stronie serwera dodaj after_filter do kontrolera sesji:

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

    Wygeneruje token, umieści go w sesji i skopiuje w nagłówku odpowiedzi dla wybranych działań.

  • Po stronie klienta (iOS) musisz upewnić się, że dwie rzeczy są miejscu.

    • Twój Klient musi przeskanować wszystkie odpowiedzi serwera w poszukiwaniu tego nagłówka i zachować go, gdy zostanie przekazany.

      ... 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];
      
    • Na koniec, twój Klient musi dodać ten token do wszystkich wysyłanych żądań "non GET":

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

Ostatnim elementem układanki jest zrozumienie, że podczas logowania do devise używane są dwie kolejne sesje / tokeny csrf. Przepływ logowania wyglądałby tak:

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/doraprojects.net/template/agent.layouts/content.php on line 54
2013-12-29 18:38:05

Twój przykład zdaje się naśladować kod z bloga Devise- https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

Jak wspomniano w tym poście, robisz to podobnie do opcji 1, która mówi, że jest niebezpieczna opcja. Myślę, że kluczem jest to, że nie chcesz po prostu resetować tokena uwierzytelniania za każdym razem, gdy użytkownik jest zapisywany. Myślę, że token powinien być utworzony jawnie (przez jakiś TokenController w API) i powinien wygasnąć okresowo.

Zauważysz, że mówię "myślę", ponieważ (o ile wiem) nikt nie ma więcej informacji na ten temat.

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2014-01-10 01:46:33

Top 10 najczęstszych luk w aplikacjach internetowych są udokumentowane w OWASP Top 10 . W tym pytaniu wspomniano, że ochrona Cross-Site Request Forgery (CSRF) została wyłączona i CSRF znajduje się na liście Owasdp Top 10. W skrócie, CSRF jest używany przez atakujących do wykonywania działań jako uwierzytelniony użytkownik. Wyłączenie ochrony CSRF doprowadzi do wystąpienia luk w zabezpieczeniach aplikacji i podważy cel posiadania bezpiecznego systemu uwierzytelniania. Jest prawdopodobne, że Ochrona CSRF zawodziła, ponieważ klient nie przekazuje tokenu synchronizacji CSRF.

Przeczytaj cały OWASP top 10, jeśli tego nie zrobisz, jest to niezwykle niebezpieczne . Zwróć szczególną uwagę na uszkodzone uwierzytelnianie i zarządzanie sesją , Sprawdź również Ściągawka zarządzania sesją .

 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/doraprojects.net/template/agent.layouts/content.php on line 54
2016-04-28 01:45:36