简体   繁体   English

这个Rails JSON身份验证API(使用Devise)是否安全?

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

My Rails app uses Devise for authentication. 我的Rails应用程序使用Devise进行身份验证。 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. 它有一个姐妹iOS应用程序,用户可以使用与Web应用程序相同的凭据登录到iOS应用程序。 So I need some kind of API for authentication. 所以我需要某种API进行身份验证。

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. 这里有许多类似的问题指向本教程 ,但它似乎已过时,因为从Devise中删除了token_authenticatable模块,并且其中一些行引发了错误。 (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. (我使用的是Devise 3.2.2。)我曾尝试根据该教程(和教程)来推出自己的教程,但我对此并不100%自信-我觉得可能有些东西被误解或错过。

Firstly, following the advice of this gist , I added an authentication_token text attribute to my users table, and the following to user.rb : 首先,按照本要点的建议,我向users表添加了authentication_token文本属性,并向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 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! .) (请注意,我的application_controller中的代码行before_filter :authenticate_user!

api/sessions_controller.rb 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 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 : 并在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 . 首先 ,您会注意到Api::SessionsController继承自Devise::RegistrationsControllerApi::RegistrationsController继承自ApiController (我还有其他一些控制器,例如Api::EventsController < ApiController ,它可以处理其他我更多的标准REST东西模型,并且与Devise没有太多联系。)这是一个非常丑陋的安排,但我想不出另一种方法来访问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. 我上面链接的教程的行include Devise::Controllers::InternalHelpers ,但是该模块似乎已在Devise的最新版本中删除。

Secondly , I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token . 其次 ,我已使用skip_before_filter :verify_authentication_token行禁用了CSRF保护。 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. 我对这是否是一个好主意感到怀疑-我看到关于JSON API是否易受CSRF攻击的很多冲突难以理解的建议-但添加这行是我可以使该死的事情起作用的唯一方法。

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. 第三 ,我想确保我了解用户登录后身份验证的工作原理。假设我有一个API调用GET /api/friends ,该API返回当前用户的朋友列表。 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, eg 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. 据我了解,iOS应用程序必须从数据库中获取用户的authentication_token (这是每个用户永远不变的固定值?),然后将其与所有请求一起作为参数提交,例如GET /api/friends?authentication_token=abcdefgh1234 ,那么我的Api::FriendsController可以执行类似User.find_by(authentication_token: params[:authentication_token])来获取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, eg when it comes to CSRF attacks? 还是有我忽略或误解的东西,例如,涉及CSRF攻击?
  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 . 特别是让一个控制器从Devise::RegistrationsController继承而另一个从ApiController继承的丑陋设计。

Thanks! 谢谢!

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. 您不想禁用CSRF,我已经读到人们认为出于某种原因它不适用于JSON API,但这是一种误解。 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添加到您的会话控制器中:

     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. 客户端(iOS),您需要确保已完成两件事。

    • 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”请求中:

       ... 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. 最后一个难题是要了解,在登录进行设计时,正在使用两个后续的session / csrf令牌。 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

Your example seems to mimic the code from the Devise blog - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6 你的例子似乎模仿从设计博客的代码- https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

As mentioned in that post, you are doing it similar to option 1, which they say is the insecure option. 如该帖子所述,您的操作类似于选项1,他们说这是不安全的选项。 I think the key is that you don't want to simply reset the authentication token every time the user is saved. 我认为关键是您不想每次保存用户时都简单地重置身份验证令牌。 I think the token should be created explicitly (by some kind of TokenController in the API) and should expire periodically. 我认为令牌应显式创建(通过API中的某种TokenController)并应定期过期。

You'll notice I say 'I think' since (as far as I can tell) nobody has any more information on this. 您会注意到我说“我认为”,因为(据我所知)没有人对此有更多信息。

The top 10 most common vulnerablites in web applications are documented in the OWASP Top 10 . OWASP Top 10中记录了Web应用程序中最常见的10个最常见的漏洞。 This question mentioned that Cross-Site Request Forgery(CSRF) protection was disabled, and CSRF is on the OWASDP Top 10 . 这个问题提到跨站请求伪造(CSRF)保护已禁用,并且CSRF处于OWASDP前10名中 In short, CSRF is used by attackers to perform actions as an authenticated user. 简而言之,攻击者使用CSRF以经过身份验证的用户身份执行操作。 Disabling CSRF protection will lead to high risk vulnerabilities in an application, and undermines the purpose of having a secure authentication system. 禁用CSRF保护将导致应用程序中的高风险漏洞,并破坏拥有安全身份验证系统的目的。 Its likely that the CSRF protection was failing, because the client is failing to pass the CSRF synchronization token. CSRF保护失败的原因很可能是因为客户端无法传递CSRF同步令牌。

Read the entire OWASP top 10, failing to do so is extremely hazardous . 阅读整个OWASP前10名,否则将非常危险 Pay close attention to Broken Authentication and Session Management , also check out the Session Management Cheat Sheet . 密切关注断开的身份验证和会话管理 ,并查看会话管理备忘单

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM