さいぬーん(´・ω・`)

Web好きプログラマの備忘録

【翻訳】devise-auth-token公式ドキュメント

deviseでトークン認証を実装する機会があり、その時にdevise-token-authを読みながら翻訳したものです。

以下翻訳↓


インストール

Gemfileに以下を追加してください:

gem 'devise_token_auth'

そしたら、bundleを使ってgemをインストールします

bundle install

次は設定...

設定

userモデルを作成し、ルーティングを定義し、concernsをincludeする必要があるでしょう、そしてこのgemのデフォルトのいくつかの設定を変更することができます。

rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]

例:

rails g devise_token_auth:install User auth

このジェネレーターは次の任意のオプションを許可します:

引数 デフォルト 概要
USER_CLASS User ユーザー認証として利用するクラス名
MOUNT_PATH auth 認証ルーティングをマウントするためのパス もっと見る

次のイベントはジェネレーターを使用すると発火します:

rake db:migrate

また、以下の項目を設定する必要があるかもしれません:

次は初期設定...

初期設定

config/initializers/devise_token_auth.rbの設定では、次の設定を利用できます:

名前 デフォルト 説明
change_headers_on_each_request true デフォルトではそれぞれのリクエストの後にaccess-tokenヘッダが変わります。クライアントは変更されるトークンを追跡し続ける責務があります。ng-token-authj-tokerの両方でこれが実行されます。これは安全ですが、管理が難しいです。この設定をfalseにすると、リクエスト毎にトークンヘッダが変更されなくなります。
token_lifespan 2.weeks トークンの有効期限を設定します。ユーザーは最後のログインからこの時間経過した後に再認証する必要があります。
batch_request_buffer_throttle 5.seconds APIに複数のリクエストを同時に行う必要があることがあります。この場合、バッチ内の各リクエストは同じ認証トークンを共有する必要があります。この設定はリクエストが同じトークンを使っている間隔を決定します。
omniauth_prefix "/omniauth" このルートは、すべてのoauth2リダイレクトコールバックの接頭辞になります。例えば、デフォルトの'/ omniauth'設定を使用すると、github oauth2プロバイダは成功した認証を'/omniauth/github/callback'にリダイレクトします。
default_confirm_success_url nil
default_password_reset_url nil
redirect_whitelist nil
enable_standard_devise_support false
remove_tokens_after_password_reset false
default_callbacks true
bypass_sign_in true

加えて、config/initializers/devise.rbを手動で作成することでそのほかのdeviseの設定をすることもできます。このファイルでできることのいくつかの例を以下に示します:

# config/initializers/devise.rb
Devise.setup do |config|
  # The e-mail address that mail will appear to be sent from
  # If absent, mail is sent from "please-change-me-at-config-initializers-devise@example.com"
  config.mailer_sender = "support@myapp.com"

  # If using rails-api, you may want to tell devise to not use ActionDispatch::Flash
  # middleware b/c rails-api does not include it.
  # See: https://stackoverflow.com/q/19600905/806956
  config.navigational_formats = [:json]
end

次はOmniAuth...

OmniAuth

OmniAuth認証

もしomniauth認証を利用したいなら、Gemfileに利用したい認証プロバイダーのgem全てを追加します。

githubfacebookgoogleを使ったomniauthの例;

# Gemfile
gem 'omniauth-github'
gem 'omniauth-facebook'
gem 'omniauth-google-oauth2'

そしたら、bundle installを実行します。

oauth2プロバイダーのリスト

OmniAuthプロバイダーの設定

config/initializers/omniauth.rbにプロバイダー毎の設定を追加します。

これらの設定はプロバイダーから取得されなければなりません。

githubfacebookgoogleを利用した例:

# config/initializers/omniauth.rb
Rails.application.config.middleware.use OmniAuth::Builder do
  provider :github,        ENV['GITHUB_KEY'],   ENV['GITHUB_SECRET'],   scope: 'email,profile'
  provider :facebook,      ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET']
  provider :google_oauth2, ENV['GOOGLE_KEY'],   ENV['GOOGLE_SECRET']
end

この例では、プロバイダのキーとシークレットキーが環境変数に格納されていることを前提としています。これを満たすには、figaro gem(またはdotenvまたはsecrets.ymlまたは同等のもの)を使用します。

OmniAuthコールバックの設定

プロバイダで設定する「コールバックURL」の設定は、このアプリケーションで定義されているomniauth接頭辞の設定に対応している必要があります。これは、クライアントアプリケーションで使用されているomniauthルートとは異なります。

例えばデモアプリケーションでは、omniauth_prefixのデフォルト設定を/omniauthにしているので、githubの「承認コールバックURL」は「https://devise-token-auth-demo.herokuapp.com/omniauth/github/callback 」に設定しなければなりません。

Githubのデモ例:

omniauth-provider-settings.png (63.5 kB)

github認証のurlはクライアント側とは異なります。クライアントはomniauth認証のために/[MOUNT_PATH]/:providerAPIにアクセスする必要があります。

例えば、アプリが次の設定を使用してマウントされていることを前提にします:

# config/routes.rb
mount_devise_token_auth_for 'User', at: 'auth'

githubのクライアント設定は次のようになります:

githubを使ったAngular.jsでの認証設定:

angular.module('myApp', ['ng-token-auth'])
  .config(function($authProvider) {
    $authProvider.configure({
      apiUrl: 'https://api.example.com'
      authProviderPaths: {
        github: '/auth/github' // <-- これはgithubで設定したものとは異なることに注意してください
      }
    });
  });

この不一致は、複数のUserクラスとマウントポイントをサポートするために必要です。

powxip.ioユーザーの方へ

もしpowまたはxip.io URLを使っている時にプロバイダーからredirect-uri-mismatchエラーを受け取ったら、開発環境の設定ファイルに次の設定を追加してください:

# config/environments/development.rb
# when using pow
OmniAuth.config.full_host = "http://app-name.dev"

# when using xip.io
OmniAuth.config.full_host = "http://xxx.xxx.xxx.app-name.xip.io"

次はメール認証....

メール認証

もしメール認証を使いたいなら、メールを送信するようにRailsアプリケーションを設定する必要があります。ここからもっと見てください。

開発環境ではmailcatcherをお勧めします。

mailcatcherの開発環境設定例

# config/environments/development.rb
Rails.application.configure do
  config.action_mailer.default_url_options = { host: 'your-dev-host.dev' }
  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = { address: 'your-dev-host.dev', port: 1025 }
end

既定のデバイステンプレートを使用する代わりにカスタム電子メールを送信する場合、これを使うこともできます。

次はデバイス言語のカスタマイズ...

バイス言語のカスタマイズ

Devise Token Authには、すべてに必要なデフォルトの文言が付属しています。しかし、これは適切ではないかもしれません。config/locales/devise.en.ymlYMLファイルを作成し、必要なカスタム値を割り当てることでdeviseのデフォルトを上書きすることができます。例えば、deviseメールの件名を変更するには以下のようにします:

# config/locales/devise.en.yml
en:
  devise:
    mailer:
      confirmation_instructions:
        subject: "Please confirm your e-mail address"
      reset_password_instructions:
        subject: "Reset password request"

次はクロスオリジンリクエスト(CORS)....

クロスオリジンリクエスト(CORS)

もしAPIとクライアントが異なるドメイン上にあるなら、Rails APIcross origin requestを許可するような設定をする必要があります。これを満たすにはrack-cors gemを使います。

次の例はいくつものドメインからリクエストを許可するのでとても危険です。必要なドメインのみをホワイトリストに登録してください。

# Gemfile
gem 'rack-cors', :require => 'rack/cors'
# config/application.rb
module YourApp
  class Application < Rails::Application
    config.middleware.use Rack::Cors do
      allow do
        origins '*'
        resource '*',
          headers: :any,
          expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'],
          methods: [:get, :post, :options, :delete, :put]
      end
    end
  end
end

Access-Control-Expose-Headersaccess-tokenexpirytoken-typeuidclientを含むことを確認してください(この例でexpose paramに設定されているように)。もしクライアントが誤った401応答をする場合、これが原因の可能性があります。

古いブラウザ(IE8、IE9)ではCORSが使用できない場合があります。通常これらのブラウザ用のプロキシを設定します。詳細については、ng-token-auth readmeまたはjToker readmeを参照してください。

使い方

クライアントが使用できるルートは次のとおりです。これらのルートは、このエンジンがマウントされているパスを基準にして実行されます(デフォルトではauth)。これらのルートはAngularJSng-token-authモジュールとjQueryjTokerプラグインで使用されるデフォルトに対応しています。

path method 目的
/ POST メール登録
/ DELETE アカウント削除
/ PUT アカウント更新
/sign_in POST メール認証
/sign_out DELETE ユーザーの現在のセッションを終了します。
/:provider GET クライアント認証の認証先としてこのルートを設定します。理想的には、これは外部のウィンドウまたはポップアップで発生します。
/:provider/callback GET / POST oauth2プロバイダのコールバックURIのリクエスト先。
/validate_token GET
/password POST | このルートを使用して、メールで登録したユーザーにパスワードリセット確認メールを送信します。
/password PUT ユーザーのパスワードを変更するには、このルートを使用します。
/password/edit GET

次はrouteのマウンティング...

routeのマウンティング

認証ルーティングはプロジェクトにマウントする必要があります。このgemにはそのためのヘルパーがあります:

mount_devise_token_auth_for - devise_forに似ています。このメソッドを使用して、ユーザー認証に必要なルートを追加します。このメソッドは、次の引数を受け取ります:

引数 デフォルト 説明
class_name string 'User' 認証に使用するクラスの名前です。
options object {at: 'auth'} 認証に使用されるルーティングには、このオブジェクトのatパラメータで指定されたパスの接頭辞が付けられます。

例:

# config/routes.rb
mount_devise_token_auth_for 'User', at: 'auth'

どのモデルクラスも使用できますが、正しく動作するためにはクラスにDeviseTokenAuth :: Concerns :: Userを含める必要があります。

このエンジンは、好きなルートにマウントすることができます。/authはデフォルトでng-token-authモジュールとjTokerプラグインのデフォルトに準拠するために使用されます。

次はコントローラー インテグレーション

コントローラー インテグレーション

Concerns

このgemはDeviseTokenAuth::Concerns::SetUserByTokenというRails concernをincludeします。これはauthenticate_user!user_signed_in?のようなコントローラーメソッドにアクセスするためのconcernをincludeします。

concernはリクエスト毎にauth tokenを変更するafter_actionも実行します。

全ての子コントローラーがconcernをincludeするように、ApplicationControllerにconcernをincludeすることを推奨します。

Concern例

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include DeviseTokenAuth::Concerns::SetUserByToken
end

メソッド

このgemは以下のdevise helper全てへのアクセスを提供します:

メソッド 説明
before_action :authenticate_user! Userがサインインしていなければ401エラーを返す
current_user 現在サインインしているUserまたは有効になっていなければnilを返す
user_signed_in? Userがサインインしていればtrueをそうでなければfalseを返す
devise_token_auth_group 複数のユーザークラスをグループとして操作する

メモ)アクセスを試みているモデルがUserではなかったら、ヘルパーメソッド名は変更されます。例えば、モデル名がAdminであれば、ヘルパーメソッドは以下のようになります:

  • before_action :authenticate_admin!
  • admin_signed_in?
  • current_admin

例:認証されたユーザーへのアクセス制限:

# app/controllers/test_controller.rb
class TestController < ApplicationController
  before_action :authenticate_user!

  def members_only
    render json: {
      data: {
        message: "Welcome #{current_user.name}",
        user: current_user
      }
    }, status: 200
  end
end

トークンヘッダー形式

認証情報はそれぞれのヘッダーにクライアントによって含まれます。ヘッダーはRFC 6750 Bearer Token形式に従います:

認証ヘッダーサンプル:

"access-token": "wwwww",
"token-type":   "Bearer",
"client":       "xxxxx",
"expiry":       "yyyyy",
"uid":          "zzzzz"

認証ヘッダーは以下のパラメーターで構成されます:

パラメーター 説明
access-token これはリクエスト毎にユーザーのパスワードとして提供されます。この値のハッシュ・バージョンは、後で比較するためにデータベースに保管されます。この値はリクエスト毎に変更されるべきです。
client これにより異なるクライアント上で複数の同時セッションを管理することができます。たとえば、携帯電話とラップトップの両方で同時に認証を受けたい場合があります。
expiry 現在のセッションが失効になる日にちです。これは、クライアントがAPIリクエストを必要とせずに期限切れのトークンを無効にするために使用できます。
uid ユーザーを識別するために使用される一意の値。アクセストークンでユーザーのDBを検索すると、APItiming attacksを受けやすくなるため、これが必要です。

各リクエストで必要な認証ヘッダは以前のリクエストからのレスポンスに使用されます。もしAngularJSのng-token-authまたはjQueryプラグインjTokerを利用しているなら、この昨日はすでに提供されています。

次はModel Integration...

Model Integration

DeviseTokenAuth::Concerns::User

このgemの典型的な使い方は以下のモデルメソッドのいつくかの使用をする必要はありません。全ての認証はController concernsによって暗黙的に処理される必要があります。

DeviseTokenAuth::Concerns::Userconcernをincludeするモデルは以下のpublicメソッドにアクセスできます。

  • valid_token?:認証トークンが有効かどうかを確認します。引数としてtokenclientを受け取り、真偽値を返します。

    :

# extract token + client_id from auth header
client_id = request.headers['client']
token = request.headers['access-token']

@resource.valid_token?(token, client_id)
  • create_new_auth_token:必要なメタデータ全てを含む新しい認証トークンを作成します。任意引数としてclientを受け取ります。何も提供されない場合新しいclientを生成します。オブジェクトとしてクライアントによって送信される必要がある認証ヘッダを返します。

# extract client_id from auth header
client_id = request.headers['client']

# update token, generate updated auth headers for response
new_auth_header = @resource.create_new_auth_token(client_id)

# update response with the header that will be required by the next request
response.headers.merge!(new_auth_header)
  • build_auth_header:次のリクエストに含めてクライアントに送信される必要がある認証ヘッダを生成します。tokenclientを引数として受け取り、文字列を返します。
# create client id and token
client_id = SecureRandom.urlsafe_base64(nil, false)
token     = SecureRandom.urlsafe_base64(nil, false)

# store client + token in user's token hash
@resource.tokens[client_id] = {
  token: BCrypt::Password.create(token),
  expiry: (Time.zone.now + @resource.token_lifespan).to_i
}

# generate auth headers for response
new_auth_header = @resource.build_auth_header(token, client_id)

# update response with the header that will be required by the next request
response.headers.merge!(new_auth_header)

次は複数ユーザークラスの使い方...

複数ユーザークラスの使い方

複数ユーザーデモ

このgemは複数ユーザーモデルの利用をサポートします。利用可能なユースケースの1つは、Userというモデルを使用して訪問者を認証し、Adminというモデルで管理者を認証することです。アプリに別の認証モデルを追加するには、次の手順を実行します:

1. 新しいモデルのためのインストールジェネレーターを実行します

rails g devise_token_auth:install Admin admin_auth

これはAdminモデルを作成し、/admin_authベースパスでモデルの認証ルーティングを定義します。

2. devise_scope内のAdminユーザーが使うルーティングを定義します。

Rails.application.routes.draw do
 # when using multiple models, controllers will default to the first available
 # devise mapping. routes for subsequent devise mappings will need to defined
 # within a `devise_scope` block

 # define :users as the first devise mapping:
 mount_devise_token_auth_for 'User', at: 'auth'

 # define :admins as the second devise mapping. routes using this class will
 # need to be defined within a devise_scope as shown below
 mount_devise_token_auth_for "Admin", at: 'admin_auth'

 # this route will authorize requests using the User class
 get 'demo/members_only', to: 'demo#members_only'

 # routes within this block will authorize requests using the Admin class
 devise_scope :admin do
   get 'demo/admins_only', to: 'demo#admins_only'
 end
end

3. 任意のAdmin限定コントローラーを設定します。コントローラーはここで定義されたメソッドへのアクセス権を持ちます。

  • before_action :authenticate_admin!
  • current_admin
  • admin_signed_in?

グループアクセス

グループを使用して同時に複数のユーザータイプへのアクセスを制御することもできます。次の例は、コントローラのアクセスをUserユーザーとAdminユーザの両方に制限する方法を示しています。

例:グループ認証

class DemoGroupController < ApplicationController
  devise_token_auth_group :member, contains: [:user, :admin]
  before_action :authenticate_member!

  def members_only
    render json: {
      data: {
        message: "Welcome #{current_member.name}",
        user: current_member
      }
    }, status: 200
  end
end

上記の例では、current_usercurrent_adminに加えて、以下のメソッドを利用することができます。

  • before_action: :authenticate_member!
  • current_member
  • member_signed_in?

次はモジュールの除外...

モジュールの除外

デフォルトではdeviseのほとんど全てのモジュールが含まれます:

これらの機能をすべてアプリで有効にしたくないかもしれません。それは大丈夫です。独自のスタイルに合わせてミックスして合わせることができます。

次の例は、電子メールの確認を無効にする方法を示しています。

例:メール認証を無効にする

DeviseTokenAuth::Concerns::Usermodel concernをincludeする前に、includeしたいモジュールをリストにするだけです。

# app/models/user.rb
class User < ActiveRecord::Base

  # notice this comes BEFORE the include statement below
  # also notice that :confirmable is not included in this block
  devise :database_authenticatable, :recoverable,
         :trackable, :validatable, :registerable,
         :omniauthable

  # note that this include statement comes AFTER the devise block above
  include DeviseTokenAuth::Concerns::User
end

いくつかの機能はアプリにマウントしたくないルーティングをincludeします。次の例は、OAuthとそのルーティングを無効にする方法を示しています。

例:OAuth認証を無効にする

まず、omniauthableモジュールをdeviseメソッドの引数リストに含めません。

# app/models/user.rb
class User < ActiveRecord::Base

  # notice that :omniauthable is not included in this block
  devise :database_authenticatable, :confirmable,
         :recoverable, :trackable, :validatable,
         :registerable

  include DeviseTokenAuth::Concerns::User
end

omniauth_callbacksコントローラーのマウントをskipするヘルパーを呼びます

# config/routes.rb
Rails.application.routes.draw do
  mount_devise_token_auth_for 'User', at: 'auth', skip: [:omniauth_callbacks]
end

次は、Controller / Mailオーバーライドのカスタマイズ

Controller / Mailオーバーライドのカスタマイズ

コントローラーオーバーライドをカスタム

組み込みコントローラーは、独自のカスタムコントローラーでオーバーライドすることができます。

例えば、TokenValidationControllervalidate_tokenメソッドのデフォルトの動作は、Userオブジェクトをjson(sansパスワードとトークンデータ)として返すことです。次の例は、モデルメソッドを含めるためにvalidate_tokenアクションをオーバーライドする方法を示しています。

例:コントローラーオーバーライド

# config/routes.rb
Rails.application.routes.draw do
  ...
  mount_devise_token_auth_for 'User', at: 'auth', controllers: {
    token_validations:  'overrides/token_validations'
  }
end

# app/controllers/overrides/token_validations_controller.rb
module Overrides
  class TokenValidationsController < DeviseTokenAuth::TokenValidationsController

    def validate_token
      # @resource will have been set by set_user_by_token concern
      if @resource
        render json: {
          data: @resource.as_json(methods: :calculate_operating_thetan)
        }
      else
        render json: {
          success: false,
          errors: ["Invalid login credentials"]
        }, status: 401
      end
    end
  end
end

レンダリングメソッドをオーバーライド

jsonレンダリングをカスタマイズするために、以下のprotected controllerメソッドを実装します、successメソッドでは@resourceオブジェクトが利用可能です。

Registrations Controller

  • render_create_error_missing_confirm_success_url
  • render_create_error_redirect_url_not_allowed
  • render_create_success
  • render_create_error
  • render_create_error_email_already_exists
  • render_update_success
  • render_update_error
  • render_update_error_user_not_found

Sessions Controller

  • render_new_error
  • render_create_success
  • render_create_error_not_confirmed
  • render_create_error_bad_credentials
  • render_destroy_success
  • render_destroy_error

Passwords Controller

  • render_create_error_missing_email
  • render_create_error_missing_redirect_url
  • render_create_error_not_allowed_redirect_url
  • render_create_success
  • render_create_error
  • render_update_error_unauthorized
  • render_update_error_password_not_required
  • render_update_error_missing_password
  • render_update_success
  • render_update_error

Token Validations Controller

  • render_validate_token_success
  • render_validate_token_error

例:デフォルト設定のコントローラーオプション

mount_devise_token_auth_for 'User', at: 'auth', controllers: {
  confirmations:      'devise_token_auth/confirmations',
  passwords:          'devise_token_auth/passwords',
  omniauth_callbacks: 'devise_token_auth/omniauth_callbacks',
  registrations:      'devise_token_auth/registrations',
  sessions:           'devise_token_auth/sessions',
  token_validations:  'devise_token_auth/token_validations'
}

注意:コントローラーオーバーライドは置き換えられると予想されるコントローラーアクションを実装するべきです

ブロックをコントローラーへ渡す

動作を完全に再実装することなく、既存のコントローラに動作を追加したいだけかもしれません。この場合、DeviseTokenAuthのコントローラーのいずれかを継承する新しいコントローラーを作成し、ブロックをsuperに渡すことで振る舞いを追加するメソッドをオーバーライドします。

class Custom::RegistrationsController < DeviseTokenAuth::RegistrationsController

  def create
    super do |resource|
      resource.do_something(extra)
    end
  end

end

ブロックはコントローラーが正常なレスポンスをレンダリングする前に実行されます。

Emailテンプレートをオーバーライド

サインアップとパスワードリセットの確認メールのデフォルトメールテンプレートをオーバーライドしたいかもしれません。次のコマンドを実行して、メールテンプレートをアプリにコピーします。

rails generate devise_token_auth:install_views

これは2つの新しいファイルを作成します

  • app/views/devise/mailer/reset_password_instructions.html.erb
  • app/views/devise/mailer/confirmation_instructions.html.erb

これらのファイルはアプリにあった編集をしても大丈夫です。このようにメールの件名をカスタマイズすることもできます。

注意:これらのテンプレートを修正する場合、むやみにlink_toブロックを変更しないでください

テスト

APIをテストする際にリクエストを承認するには、リクエストに4つのヘッダを渡す必要があります。これらのヘッダに適切な値を取得する最も簡単な方法は、resource.create_new_auth_tokenを使用することです。

  request.headers.merge! resource.create_new_auth_token
  get '/api/authenticated_resource'
  # success

問題や疑問がある場合は#75をチェックしてください。

Rspecでテスト

一般的なリクエスト仕様

以下は、テストの一般的な例です

# I've called it authentication_test_spec.rb and placed it in the spec/requests folder
require 'rails_helper'
include ActionController::RespondWith

# The authentication header looks something like this:
# {"access-token"=>"abcd1dMVlvW2BT67xIAS_A", "token-type"=>"Bearer", "client"=>"LSJEVZ7Pq6DX5LXvOWMq1w", "expiry"=>"1519086891", "uid"=>"darnell@konopelski.info"}

describe 'Whether access is ocurring properly', type: :request do
  before(:each) do
    @current_user = FactoryBot.create(:user)
    @client = FactoryBot.create :client
  end

  context 'context: general authentication via API, ' do
    it "doesn't give you anything if you don't log in" do
      get api_client_path(@client)
      expect(response.status).to eq(401)
    end

    it 'gives you an authentication code if you are an existing user and you satisfy the password' do
      login
      # puts "#{response.headers.inspect}"
      # puts "#{response.body.inspect}"
      expect(response.has_header?('access-token')).to eq(true)
    end

    it 'gives you a status 200 on signing in ' do
      login
      expect(response.status).to eq(200)
    end

    it 'gives you an authentication code if you are an existing user and you satisfy the password' do
      login
      expect(response.has_header?('access-token')).to eq(true)
    end

    it 'first get a token, then access a restricted page' do
      login
      auth_params = get_auth_params_from_login_response_headers(response)
      new_client = FactoryBot.create(:client)
      get api_find_client_by_name_path(new_client.name), headers: auth_params
      expect(response).to have_http_status(:success)
    end

    it 'deny access to a restricted page with an incorrect token' do
      login
      auth_params = get_auth_params_from_login_response_headers(response).tap do |h|
        h.each do |k, _v|
          if k == 'access-token'
            h[k] = '123'
          end end
      end
      new_client = FactoryBot.create(:client)
      get api_find_client_by_name_path(new_client.name), headers: auth_params
      expect(response).not_to have_http_status(:success)
    end
  end

  RSpec.shared_examples 'use authentication tokens of different ages' do |token_age, http_status|
    let(:vary_authentication_age) { token_age }

    it 'uses the given parameter' do
      expect(vary_authentication_age(token_age)).to have_http_status(http_status)
    end

    def vary_authentication_age(token_age)
      login
      auth_params = get_auth_params_from_login_response_headers(response)
      new_client = FactoryBot.create(:client)
      get api_find_client_by_name_path(new_client.name), headers: auth_params
      expect(response).to have_http_status(:success)

      allow(Time).to receive(:now).and_return(Time.now + token_age)

      get api_find_client_by_name_path(new_client.name), headers: auth_params
      response
    end
  end

  context 'test access tokens of varying ages' do
    include_examples 'use authentication tokens of different ages', 2.days, :success
    include_examples 'use authentication tokens of different ages', 5.years, :unauthorized
  end

  def login
    post api_user_session_path, params:  { email: @current_user.email, password: 'password' }.to_json, headers: { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' }
  end

  def get_auth_params_from_login_response_headers(response)
    client = response.headers['client']
    token = response.headers['access-token']
    expiry = response.headers['expiry']
    token_type = response.headers['token-type']
    uid = response.headers['uid']

    auth_params = {
      'access-token' => token,
      'client' => client,
      'uid' => uid,
      'expiry' => expiry,
      'token_type' => token_type
    }
    auth_params
  end
end

初めから承認ヘッダーを作成する方法

require 'rails_helper'
include ActionController::RespondWith

def create_auth_header_from_scratch
  # You need to set up factory bot to use this method
  @current_user = FactoryBot.create(:user)
  # create client id and token
  client_id = SecureRandom.urlsafe_base64(nil, false)
  token     = SecureRandom.urlsafe_base64(nil, false)

  # store client + token in user's token hash
  @current_user.tokens[client_id] = {
    token: BCrypt::Password.create(token),
    expiry: (Time.now + 1.day).to_i
  }

  # Now we have to pretend like an API user has already logged in.
  # (When the user actually logs in, the server will send the user
  # - assuming that the user has  correctly and successfully logged in
  # - four auth headers. We are to then use these headers to access
  # things which are typically restricted
  # The following assumes that the user has received those headers
  # and that they are then using those headers to make a request

  new_auth_header = @current_user.build_auth_header(token, client_id)

  puts 'This is the new auth header'
  puts new_auth_header.to_s

  # update response with the header that will be required by the next request
  puts response.headers.merge!(new_auth_header).to_s
end

リクエストspecの追加例

FAQ

このgemを標準のdeviseと一緒に使えますか?

はい!しかし、標準のDeviseと別々のルーティングのサポートを有効にする必要があります。だから、このようになります:

# config/initializers/devise_token_auth.rb
DeviseTokenAuth.setup do |config|
  config.enable_standard_devise_support = true
end
# config/routes.rb
Rails.application.routes.draw do

  # standard devise routes available at /users
  # NOTE: make sure this comes first!!!
  devise_for :users

  # token auth routes available at /api/v1/auth
  namespace :api do
    scope :v1 do
      mount_devise_token_auth_for 'User', at: 'auth'
    end
  end

end

2018年5月にアップデートされたdeviseと一緒に使う別の方法

何人かのユーザーがconfig.enable_standard_devise_support = trueとすることでdeviseと一緒に利用する際に問題が発生しています。

jotoloが示唆するもう1つの方法は、DeviseTokenAuthまたは標準のDeviseを使用する別々の子のapplication_controller.rbファイルを作成することです。これらのファイルはすべて、基本のapplication_controller.rbファイルを継承します。

例えば、アプリケーションのAPI(Devise Token Authを使用する)のapi/v1/application_controller.rbファイルと、アプリケーションの完全なスタック部分のadmin/application_controller.rbファイル(標準のDevise )を作ります。アプリケーションの各フローを適切な子のapplication_controller.rbファイルにリダイレクトすること

DeviseTokenAuthを使うAPIの子アプリケーションコントローラー

# controllers/api/v1/application_controller.rb
module Api
  module V1
    class ApplicationController < ::ApplicationController
      skip_before_action :verify_authenticity_token
      include DeviseTokenAuth::Concerns::SetUserByToken
    end
  end
end

標準のdeviseを使うフルスタック部分の子アプリケーションコントローラー

# controllers/admin/application_controller.rb
module Admin
  class ApplicationController < ::ApplicationController
    before_action :authenticate_admin!
  end
end

ベースとなるアプリケーションコントローラーです。CSRFトークン保護を使用している場合は、API固有のアプリケーションコントローラ(api/v1/application_controller.rb)でスキップできます。

# controllers/application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
end

enable_standard_devise_support設定コメントアウトfalseのままにしておきます。

# config/initializers/devise_token_auth.rb

# config.enable_standard_devise_support = false

すでにユーザーが存在していて、どのように新しいフィールドを追加できますか?

  1. まず、rails g devise_token_auth:install [USER_CLASS] [MOUNT_PATH]コマンドで生成されるマイグレーションファイルを削除します。それから
  2. 別の新しいマイグレーションファイルを作成します
  # create migration by running a command like this (where `User` is your USER_CLASS table):
  # `rails g migration AddTokensToUsers provider:string uid:string tokens:text`

  def up
    add_column :users, :provider, :string, null: false, default: 'email'
    add_column :users, :uid, :string, null: false, default: ''
    add_column :users, :tokens, :text

    # if your existing User model does not have an existing **encrypted_password** column uncomment below line.
    # add_column :users, :encrypted_password, :null => false, :default => ""

    # the following will update your models so that when you run your migration

    # updates the user table immediately with the above defaults
    User.reset_column_information

    # finds all existing users and updates them.
    # if you change the default values above you'll also have to change them here below:
    User.find_each do |user|
      user.uid = user.email
      user.provider = 'email'
      user.save!
    end

    # to speed up lookups to these columns:
    add_index :users, [:uid, :provider], unique: true
  end

  def down
    # if you added **encrypted_password** above, add here to successfully rollback
    remove_columns :users, :provider, :uid, :tokens
  end

概念図

このgemを使用するには以下の情報は必要ありませんが、好奇心が強い場合はこの情報をお読みください。

トークン管理について

トークンは、APIへのリクエストごとに無効にする必要があります。次の図は、この概念を示しています:

token-update-detail.jpg (42.9 kB)

各リクエストの間に、新しいトークンが生成されます。次のリクエストで使用されるべきaccess-tokenヘッダが前のリクエストに対するレスポンスのaccess-tokenヘッダに戻されます。ダイアグラム内の最後の要求は、以前のリクエストによって無効化されたトークンを使用しようとするため、失敗します。

期限切れのトークンが許可される唯一のケースは、batch requests中のみです。

これらの措置は、このgemを使用するときにデフォルトで採用されます。

batch requestsについて

デフォルトでは、APIは要求ごとに認証トークンを更新する必要があります(詳細はこちらを参照)。ただし、APIに複数の同時リクエストを行う必要がある場合があります。

Batch request例

$scope.getResourceData = function() {

  $http.get('/api/restricted_resource_1').success(function(resp) {
    // handle response
    $scope.resource1 = resp.data;
  });

  $http.get('/api/restricted_resource_2').success(function(resp) {
    // handle response
    $scope.resource2 = resp.data;
  });
};

この場合、最初のレスポンスが完了する前に2番目のリクエストが開始されるため、2番目のリクエストのaccess-tokenヘッダーを最初のリクエストのaccess-tokenヘッダーで更新することは不可能です。サーバーは、これらの並行リクエストのバッチが同じ認証トークンを共有できるようにする必要があります。この図は、batch requestsがサーバーによってどのように識別されるかを示しています。

batch-request-overview.jpg (55.7 kB)

ダイアグラムの "5秒"バッファは、このgemで使用されるデフォルトです。

次の図は、batch requestsを処理する際に使用されるクライアント、サーバー、およびアクセストークンの関係を示しています。

batch-request-detail.jpg (33.6 kB)

サーバーがリクエストがbatch requestsの一部であると識別した場合、ユーザーの認証トークンは更新されません。認証トークンは更新され、バッチ内の最初のリクエストと共に返され、バッチ内の後続のリクエストはトークンを返しません。これは、応答の順序がクライアントに保証されないため、最後の有効なトークンが返された後にクライアントが古いトークンを受け取らないようにする必要があるためです。

このgemはバッチリクエストを自動的に管理します。config/initializers/devise_token_auth.rbbatch_request_buffer_throttleパラメータを使用して、バッチリクエストとみなされる時間バッファを変更することができます。


長かった!w