这个 Rails JSON 身份验证 API(使用 Devise)安全吗? [英] Is this Rails JSON authentication API (using Devise) secure?

查看:27
本文介绍了这个 Rails JSON 身份验证 API(使用 Devise)安全吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

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.

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

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. (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.

首先,根据这个要点的建议,我添加了一个authentication_tokenusers 表的 code> 文本属性,以及 user.rb 的以下内容:

Firstly, following the advice of this gist, I added an authentication_token text attribute to my users table, and the following to 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_controller 有一行 before_filter :authenticate_user!.)

(Note that my application_controller has the line 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

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:

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

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. 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.

其次,我使用 skip_before_filter :verify_authentication_token 行禁用了 CSRF 保护.我怀疑这是否是个好主意 - 我看到很多 冲突难以理解关于 JSON 的建议API 容易受到 CSRF 攻击 - 但添加该行是我让该死的东西工作的唯一方法.

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 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.

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

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. 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, e.g. 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. 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. 这个登录系统安全吗?或者是我忽略或误解了什么,例如当涉及 CSRF 攻击时?
  2. 我对如何在用户登录后对请求进行身份验证的理解正确吗?(请参阅上面的第三...".)
  3. 有什么办法可以清理或改进这段代码?特别是让一个控制器继承自 Devise::RegistrationsController 而其他控制器继承自 Devise::RegistrationsController 的丑陋设计.code>ApiController.
  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::RegistrationsController and the others from ApiController.

谢谢!

推荐答案

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

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:

  • 在服务器端向会话控制器添加一个 after_filter:

  • 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.

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

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];

  • 最后,您的客户端需要将此令牌添加到它发出的所有非 GET"请求中:

  • 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"];
    

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

    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:

    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
    

    这篇关于这个 Rails JSON 身份验证 API(使用 Devise)安全吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆