web-dev-qa-db-ja.com

Grape and Deviseによるユーザー認証

APIでUser Authenticationを理解して適切に実装するのが困難です。つまり、Grape APIとBackbone.js、AngularJS、Ember.jsなどのフロントエンドフレームワークとの統合を理解するのに深刻な問題があります。

私はさまざまなアプローチをピボットし、それについてたくさん読んでいますが、Googleは本当に悪いリソースを返し、このトピックに関する本当に良い記事はないようです-RailsとユーザーDeviseおよびフロントエンドフレームワークによる認証

私の現在の要点について説明します。私の実装に関するフィードバックを提供していただければ、正しい方向に向けることができます。

現在の実装

私はバックエンドを持っていますRails REST API with following Gemfile(私はすべて意図的に短縮しますファイルコード)

gem 'Rails', '4.1.6'
gem 'mongoid', '~> 4.0.0'
gem 'devise'
gem 'grape'
gem 'rack-cors', :require => 'rack/cors'

現在の実装には、次のルートを持つAPIのみがあります(routes.rb):

api_base      /api        API::Base
     GET        /:version/posts(.:format)
     GET        /:version/posts/:id(.:format)
     POST       /:version/posts(.:format)
     DELETE     /:version/posts/:id(.:format)
     POST       /:version/users/authenticate(.:format)
     POST       /:version/users/register(.:format)
     DELETE     /:version/users/logout(.:format)

私は次のモデルを作成しましたuser.rb

class User
  include Mongoid::Document
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  field :email,              type: String, default: ""
  field :encrypted_password, type: String, default: ""

  field :authentication_token,  type: String

  before_save :ensure_authentication_token!

  def ensure_authentication_token!
    self.authentication_token ||= generate_authentication_token
  end

  private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.where(authentication_token: token).first
    end
  end   
end

私のコントローラーでは、次のフォルダー構造を作成しました:controllers-> api-> v1そして、次の共有モジュール認証(authentication。 rb

module API
  module V1
    module Authentication
      extend ActiveSupport::Concern

      included do
        before do
           error!("401 Unauthorized", 401) unless authenticated?
         end

         helpers do
           def warden
             env['warden']
           end

           def authenticated?
             return true if warden.authenticated?
             params[:access_token] && @user = User.find_by(authentication_token: params[:access_token])
           end

           def current_user
             warden.user || @user
           end
         end
       end
     end
   end
end

したがって、リソースが認証トークンで呼び出されることを確認するたびに、次のように呼び出すだけでこれを追加できます:include API::V1::AuthenticationをGrapeリソースに追加します。

module API
  module V1
    class Posts < Grape::API
      include API::V1::Defaults
      include API::V1::Authentication

これで、Users(users.rb)と呼ばれる別のGrapeリソースができました。ここで、認証、登録、ログアウトのメソッドを実装します(ここではリンゴと梨を混ぜていると思います。ログイン/ログアウトプロセスを別のGrapeリソースに抽出する必要があります-セッション)。

module API
  module V1
    class Users < Grape::API
      include API::V1::Defaults

      resources :users do
        desc "Authenticate user and return user object, access token"
        params do
           requires :email, :type => String, :desc => "User email"
           requires :password, :type => String, :desc => "User password"
         end
         post 'authenticate' do
           email = params[:email]
           password = params[:password]

           if email.nil? or password.nil?
             error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
             return
           end

           user = User.find_by(email: email.downcase)
           if user.nil?
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
              return
           end

           if !user.valid_password?(password)
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
              return
           else
             user.ensure_authentication_token!
             user.save
             status(201){status: 'ok', token: user.authentication_token }
           end
         end

         desc "Register user and return user object, access token"
         params do
            requires :first_name, :type => String, :desc => "First Name"
            requires :last_name, :type => String, :desc => "Last Name"
            requires :email, :type => String, :desc => "Email"
            requires :password, :type => String, :desc => "Password"
          end
          post 'register' do
            user = User.new(
              first_name: params[:first_name],
              last_name:  params[:last_name],
              password:   params[:password],
              email:      params[:email]
            )

            if user.valid?
              user.save
              return user
            else
              error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
            end
          end

          desc "Logout user and return user object, access token"
           params do
              requires :token, :type => String, :desc => "Authenticaiton Token"
            end
            delete 'logout' do

              user = User.find_by(authentication_token: params[:token])

              if !user.nil?
                user.remove_authentication_token!
                status(200)
                {
                  status: 'ok',
                  token: user.authentication_token
                }
              else
                error!({:error_code => 404, :error_message => "Invalid token."}, 401)
              end
            end
      end
    end
  end
end

私はここに大量のコードを提示し、それが意味をなさないかもしれないことを理解していますが、これは私が現在持っているものであり、authentication_tokenモジュールAuthenticationで保護されているAPIに対する呼び出し。

このソリューションは良いとは思えませんが、APIを介してユーザー認証を実現する簡単な方法を本当に探しています。以下に挙げる質問がいくつかあります。

質問

  1. この種の実装は危険だと思いますか、もしそうなら、なぜですか? -トークンを1つ使用しているためだと思います。このパターンを改善する方法はありますか?有効期限などが設定された別のモデルTokenを使用した実装も確認しました。ただし、この目的のためにOAuth2を実装できるため、これはホイールを再発明するようなものだと思います。もっと軽い解決策が欲しいのですが。
  2. 認証用の新しいモジュールを作成し、それを必要なリソースにのみ含めることは良い習慣ですか?
  3. このトピックに関する優れたチュートリアルについて知っていますか-Rails + Devise + Grape?)さらに、優れたオープンソースRailsプロジェクト、この方法で実装されているのはどれですか?
  4. より安全な別のアプローチでどのように実装できますか?

投稿が長かったことをお詫びしますが、同じ問題を抱えている人が増えて、質問に対する回答が増えることを期待しています。

21
Tom Hert

Token_authenticableをdeviseモジュールに追加します(deviseバージョン<= 3.2で動作)

User.rbで、:token_authenticatableをdeviseモジュールのリストに追加すると、以下のようになります。

_class User < ActiveRecord::Base
# ..code..
  devise :database_authenticatable,
    :token_authenticatable,
    :invitable,
    :registerable,
    :recoverable,
    :rememberable,
    :trackable,
    :validatable

  attr_accessible :name, :email, :authentication_token

  before_save :ensure_authentication_token
# ..code..
end
_

自分で認証トークンを生成する(デバイスバージョン> 3.2の場合)

_class User < ActiveRecord::Base
# ..code..
  devise :database_authenticatable,
    :invitable,
    :registerable,
    :recoverable,
    :rememberable,
    :trackable,
    :validatable

  attr_accessible :name, :email, :authentication_token

  before_save :ensure_authentication_token

  def ensure_authentication_token
    self.authentication_token ||= generate_authentication_token
  end

  private

  def generate_authentication_token
    loop do
      token = Devise.friendly_token
      break token unless User.where(authentication_token: token).first
    end
  end
_

認証トークンの移行を追加する

_Rails g migration add_auth_token_to_users
      invoke  active_record
      create    db/migrate/20141101204628_add_auth_token_to_users.rb
_

移行ファイルを編集して、ユーザーに:authentication_token列を追加します

_class AddAuthTokenToUsers < ActiveRecord::Migration
  def self.up
    change_table :users do |t|
      t.string :authentication_token
    end

    add_index  :users, :authentication_token, :unique => true
  end

  def self.down
    remove_column :users, :authentication_token
  end
end
_

移行を実行する

_rake db:migrate_

既存のユーザーのトークンを生成する

ユーザーごとに認証トークンが確実に存在するように、ユーザーのすべてのインスタンスでsaveを呼び出す必要があります。

User.all.each(&:save)

認証トークンを使用した安全なGrape API

トークンベースの認証を追加するには、以下のコードをAPI :: Rootに追加する必要があります。 API :: Rootを理解していない場合は、 Grapeを使用したRESTful APIの構築 をお読みください。

以下の例では、2つのシナリオに基づいてユーザーを認証しています–ユーザーがWebアプリにログオンしている場合は、同じセッションを使用します–セッションが利用できず、認証トークンが渡された場合は、トークンに基づいてユーザーを検索します

_# lib/api/root.rb
module API
  class Root < Grape::API
    prefix    'api'
    format    :json

    rescue_from :all, :backtrace => true
    error_formatter :json, API::ErrorFormatter

    before do
      error!("401 Unauthorized", 401) unless authenticated
    end

    helpers do
      def warden
        env['warden']
      end

      def authenticated
        return true if warden.authenticated?
        params[:access_token] && @user = User.find_by_authentication_token(params[:access_token])
      end

      def current_user
        warden.user || @user
      end
    end

    mount API::V1::Root
    mount API::V2::Root
  end
end
_
21
MZaragoza

@MZaragozaの質問と回答は気に入っていますが、token_authenticalが理由によりDeviseから削除されたことは注目に値します。トークンの使用は、タイミング攻撃に対して脆弱です。 この投稿Deviseのブログ も参照してください。そのため、@ MZaragozaの回答に賛成していません。

APIをDoorkeeperと組み合わせて使用​​する場合、同様のことを実行できますが、Userテーブル/モデルでauthentication_tokenを確認する代わりに、OauthAccessTokensテーブルでトークンを探します。

def authenticated
   return true if warden.authenticated?
   params[:access_token] && @user = OauthAccessToken.find_by_token(params[:access_token]).user
end

そのトークン(つまり、実際のaccess_token)は一定の時間だけ存在するため、これはより安全です。

これを行うには、次のように、ユーザーモデルとOauthAccessTokenモデルが必要です。

class User < ActiveRecord::Base

   has_many :oauth_access_tokens

end

class OauthAccessToken < ActiveRecord::Base
    belongs_to :user, foreign_key: 'resource_owner_id'
end

EDIT:また、一般的には、URLにaccess_tokenを含めないでください: http://tools.ietf.org/html /draft-ietf-oauth-v2-bearer-16#section-2.

3
PSR