Ruby-on-rails 这个 Rails JSON 身份验证 API(使用 Devise)安全吗?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20745843/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-02 23:09:23  来源:igfitidea点击:

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

ruby-on-railsjsonapisecuritydevise

提问by GMA

My Rails app uses Devise for authentication. 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. So I need some kind of API for authentication.

我的 Rails 应用程序使用 Devise 进行身份验证。它有一个姊妹 iOS 应用程序,用户可以使用他们用于 Web 应用程序的相同凭据登录到 iOS 应用程序。所以我需要某种 API 来进行身份验证。

Lots of similar questions on here point to this tutorial, but it seems to be out-of-date, as the token_authenticatablemodule has since been removed from Devise and some of the lines throw errors. (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.

这里有很多类似的问题指向本教程,但它似乎token_authenticatable已经过时了,因为该模块已从 Devise 中删除,并且一些行抛出错误。(我正在使用 Devise 3.2.2。)我试图根据那个教程(和这个教程)推出我自己的教程,但我不是 100% 有信心 - 我觉得可能有些东西我已经被误解或错过。

Firstly, following the advice of this gist, I added an authentication_tokentext attribute to my userstable, and the following to user.rb:

首先,按照这个 gist的建议,我authentication_token在我的users表中添加了一个text 属性,并将以下内容添加到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_controllerhas 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::SessionsControllerinherits from Devise::RegistrationsControllerwhereas Api::RegistrationsControllerinherits from ApiController(I also have some other controllers such as Api::EventsController < ApiControllerwhich 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. 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.

首先,您会注意到Api::SessionsController继承自Devise::RegistrationsControllerApi::RegistrationsController继承自ApiController(我还有一些其他控制器,例如Api::EventsController < ApiController它们为我的其他模型处理更标准的 REST 内容,并且与 Devise 没有太多联系。)这是一个非常丑陋的安排,但我想不出另一种方法来访问我需要的方法Api::RegistrationsController。我上面链接的教程有一行include Devise::Controllers::InternalHelpers,但这个模块似乎已在较新版本的 Devise 中删除。

Secondly, I've disabled CSRF protection with the line skip_before_filter :verify_authentication_token. I have my doubts about whether this is a good idea - I see a lot of conflictingor hard to understandadvice 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.

其次,我已经禁用了 CSRF 保护skip_before_filter :verify_authentication_token。我怀疑这是否是一个好主意 - 我看到了很多关于 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/friendswhich returns a list of the current user's friends. As I understand it, the iOS app would have to get the user's authentication_tokenfrom the database (which is a fixed value for each user that never changes??), then submit it as a param along with every request, e.g. GET /api/friends?authentication_token=abcdefgh1234, then my Api::FriendsControllercould do something like User.find_by(authentication_token: params[:authentication_token])to get the current_user. Is it really this simple, or am I missing something?

第三,我想确保我了解用户登录后身份验证是如何工作的。假设我有一个 API 调用GET /api/friends,它返回当前用户的朋友列表。据我了解,iOS 应用程序必须authentication_token从数据库中获取用户的(这是每个用户的固定值,永远不会改变??),然后将其作为参数与每个请求一起提交,例如GET /api/friends?authentication_token=abcdefgh1234,然后我Api::FriendsController可以做类似于User.find_by(authentication_token: params[:authentication_token])获取current_user。真的这么简单,还是我错过了什么?

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, e.g. when it comes to CSRF attacks?
  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::RegistrationsControllerand the others from ApiController.
  1. 这个登录系统安全吗?或者我是否忽略或误解了某些东西,例如当涉及 CSRF 攻击时?
  2. 一旦用户登录正确,我是否理解如何对请求进行身份验证?(参见上面的“第三……”。)
  3. 有什么办法可以清理或改进这段代码?特别是让一个控制器Devise::RegistrationsControllerApiController.

Thanks!

谢谢!

采纳答案by beno1604

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. To keep it enabled, you want to make a few changes:

您不想禁用 CSRF,我读过人们认为出于某种原因它不适用于 JSON API,但这是一种误解。要保持启用状态,您需要进行一些更改:

  • on there server side add a after_filter to your sessions controller:

    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.

    • 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 ahold of your request object
      if (yourConfig.csrfToken && ![request.httpMethod isEqualToString:@"GET"])
        [request setValue:yourConfig.csrfToken forHTTPHeaderField:@"X-CSRF-Token"];
      
  • 在那里服务器端添加一个 after_filter 到您的会话控制器:

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

    这将生成一个令牌,将其放入您的会话中并将其复制到所选操作的响应标头中。

  • 客户端(iOS),您需要确保两件事到位。

    • 您的客户端需要扫描此标头的所有服务器响应,并在传递时保留它。

      ... 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];
      
    • 最后,您的客户端需要将此令牌添加到它发出的所有“非 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. A login flow would look like this:

最后一个难题是要了解在登录设计时,将使用两个后续会话/csrf 令牌。登录流程如下所示:

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

回答by Jaco Pretorius

Your example seems to mimic the code from the Devise blog - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

您的示例似乎模仿了 Devise 博客中的代码 - 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. 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.

正如在那篇文章中提到的,您正在执行类似于选项 1 的操作,他们说这是不安全的选项。我认为关键是您不想在每次保存用户时简单地重置身份验证令牌。我认为应该显式创建令牌(通过 API 中的某种 TokenController)并且应该定期过期。

You'll notice I say 'I think' since (as far as I can tell) nobody has any more information on this.

你会注意到我说“我认为”,因为(据我所知)没有人有更多关于这方面的信息。

回答by rook

The top 10 most common vulnerablites in web applications are documented in the OWASP Top 10. This question mentioned that Cross-Site Request Forgery(CSRF) protection was disabled, and CSRF is on the OWASDP Top 10. In short, CSRF is used by attackers to perform actions as an authenticated user. Disabling CSRF protection will lead to high risk vulnerabilities in an application, and undermines the purpose of having a secure authentication system. Its likely that the CSRF protection was failing, because the client is failing to pass the CSRF synchronization token.

Web 应用程序中最常见的 10 个漏洞记录在OWASP Top 10 中。这个问题提到跨站点请求伪造(CSRF)保护被禁用,CSRF在OWASDP Top 10。简而言之,攻击者使用 CSRF 作为经过身份验证的用户执行操作。禁用 CSRF 保护将导致应用程序中存在高风险漏洞,并破坏拥有安全身份验证系统的目的。很可能是 CSRF 保护失败了,因为客户端没有通过 CSRF 同步令牌。

Read the entire OWASP top 10, failing to do so is extremely hazardous. Pay close attention to Broken Authentication and Session Management, also check out the Session Management Cheat Sheet.

阅读整个 OWASP 前 10 名,不这样做是非常危险的。密切关注Broken Authentication 和 Session Management,同时查看Session Management Cheat Sheet