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

Posted

技术标签:

【中文标题】这个 Rails JSON 身份验证 API(使用 Devise)安全吗?【英文标题】:Is this Rails JSON authentication API (using Devise) secure? 【发布时间】:2014-01-11 19:23:27 【问题描述】:

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

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

首先,根据this gist 的建议,我在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

然后我有以下控制器:

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

(请注意,我的application_controllerbefore_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

config/routes.rb 中:

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

我有点不知所措,但我敢肯定,我未来的自己会在这里回顾某些事情并畏缩(通常是这样)。一些不确定的部分:

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

其次,我已经通过skip_before_filter :verify_authentication_token 行禁用了CSRF 保护。我怀疑这是否是一个好主意——我看到很多关于 JSON API 是否容易受到 CSRF 攻击的conflicting 或hard to understand 建议——但添加该行是我能得到该死的东西的唯一方法工作。

第三,我想确保了解用户登录后身份验证的工作原理。假设我有一个 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。真的这么简单,还是我遗漏了什么?

因此,对于任何设法一直阅读到这个庞大问题结尾的人,感谢您抽出宝贵的时间!总结一下:

    这个登录系统安全吗? 或者有什么我忽略或误解的东西,例如当涉及到 CSRF 攻击? 我对用户登录后如何验证请求的理解是否正确?(请参阅上面的“第三...”。) 有什么办法可以清理或改进这段代码吗? 特别是丑陋的设计,让一个控制器从Devise::RegistrationsController 继承而其他控制器从ApiController 继承。

谢谢!

【问题讨论】:

您的 Api::SessionsController 是从 Devise::RegistrationsController 扩展而来的.. 【参考方案1】:

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

在服务器端添加一个 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"];

最后一个难题是要了解当登录到设计时,正在使用两个后续会话/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

【讨论】:

谢谢,这真的很有帮助。那么我是否正确地认为,一旦我登录,我需要从响应中获取 auth_token,然后将其与后续请求一起传递以验证该用户的身份? 如果您像我展示的那样修改您的客户端发送它的(非 GET)请求的方式,这将自动完成。顺便说一句,这一切都假设您使用设计的默认基于会话的身份验证。如果您使用令牌进行身份验证,则需要不同的登录流程,不确定那里的详细信息。 所以澄清一下,这是怎么回事? 1) iOS 应用调用GET /users/sign_in 并获取CSRF 令牌。 2) 它使用刚刚收到的 CSRF 令牌提交给POST /users/sign_in,并获得一个新的 CSRF 令牌。这还会在 iOS 应用程序上保存一个 cookie 并创建一个新会话。 3) 对于所有未来的请求,iOS 应用程序使用 cookie 进行身份验证,此外它还包含用于保护非 GET 请求的 CSRF 令牌。我说的对吗? 是的,基本上就是这样。澄清一下,cookies/sessions 被用于匿名和登录状态。 如果您根本不使用 cookie/会话怎么办?为什么你会关心 CSRF 而不是?只需要关心XSS即可。【参考方案2】:

您的示例似乎模仿了 Devise 博客中的代码 - https://gist.github.com/josevalim/fb706b1e933ef01e4fb6

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

您会注意到我说“我认为”,因为(据我所知)没有人对此有任何更多信息。

【讨论】:

【参考方案3】:

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

阅读整个 OWASP 前 10 名,否则会非常危险。密切关注Broken Authentication and Session Management,同时查看Session Management Cheat Sheet。

【讨论】:

这些如何应用于 RESTful API? RESTful API 不使用会话!黑客必须对 API 使用 javascript 调用,该 API 必须访问 HTTP 本地存储或站点 cookie——这基本上需要在您的站点上获取恶意脚本,此时似乎有很多更简单的方法攻击系统。

以上是关于这个 Rails JSON 身份验证 API(使用 Devise)安全吗?的主要内容,如果未能解决你的问题,请参考以下文章

Rails JSON API 的简单令牌认证注销

Rails API 用户身份验证接受来自 React 代理的 api 调用,但不直接来自主机 url

rails 3中的api身份验证

Rails:在开发中禁用敲击 JWT 身份验证

使用 Rails 和 HTTParty 将 JSON 发布到 API

Rails api 和本机移动应用程序身份验证