devise_token_authのEmail authenticationでアカウント作成からログインまで

devise_token_authを使った認証が自分にとって定番パターンになりつつあるので手順を残しておきます。

RailsAPIモードで、クライアントはnode.jsなどで別オリジンで動いているという想定です。


Gemを追加
gem 'devise'
gem 'devise_token_auth'
gem 'rack-cors'
$ bundle install
devise:install
$ bundle exec rails g devise:install
config/environments/development.rb にメール関連の設定を追加
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = { address: 'localhost', port: 1025 }
Userモデルを作成

以下を実行。別のモデル名を使いたい場合は適宜変えてください

$ bundle exec rails g devise_token_auth:install User auth
migrate

migrationファイルにTrackable関連を追加(モデル側ではデフォルトでオンになっているけどmigrationにはデフォルトで書きこんでくれない罠があって、これを忘れるとログイン時にエラーが出る)

## Trackable
t.integer  :sign_in_count, default: 0, null: false
t.datetime :current_sign_in_at
t.datetime :last_sign_in_at
t.string   :current_sign_in_ip
t.string   :last_sign_in_ip
$ bundle exec rails db:migrate
config/initializers/devise_token_auth.rb を編集

config.change_headers_on_each_requestをfalseにしておくのと、config.token_lifespanを2.weeksにしておく(この設定は単に僕の好みです)。

それからdefault_confirm_success_urlも追加します。

config.change_headers_on_each_request = false
config.token_lifespan = 2.weeks

# confirm後ここに書いたURLにリダイレクトされます。僕の場合localhost:5000でnode.jsのアプリケーションが動いているのここに飛ばします。
if Rails.env.development?
  config.default_confirm_success_url = 'http://localhost:5000/welcome'
else
  config.default_confirm_success_url = 'https://www.example.com'
end
User.rbを修正
class User < ActiveRecord::Base
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable,
+         :confirmable
  include DeviseTokenAuth::Concerns::User

+  def will_save_change_to_email?
+    super
+  end

end

これはEmailが送信されない問題の対策です。そのうち不要になるかもしれないです。

github.com

mailcatcher

メール送信を確認するためにmailcatcherを起動しておく(ない場合はgem install mailcatcher)

$ mailcatcher
rails s
$ bundle exec rails s
アカウント作成

とりあえずcurl

$ curl localhost:3000/auth -X POST -d '{"email":"example@example.com", "password":"testpassword", "password_confirmation": "testpassword"}' -H "content-type:application/json"

レスポンス

{"status":"success","data":{"id":1,"provider":"email","uid":"example@example.com","allow_password_change":false,"name":null,"nickname":null,"image":null,"email":"example@example.com","created_at":"2019-11-15T13:40:48.852Z","updated_at":"2019-11-15T13:40:48.852Z"}}

ちなみにここでパスワードとパスワード(確認用)が一致していないなど不正なパラメータを投げると以下のようなレスポンスが返ってきます

{"status":"error","data":{"id":null,"provider":"email","uid":"","allow_password_change":false,"name":null,"nickname":null,"image":null,"email":"example1@example.com","created_at":null,"updated_at":null},"errors":{"password_confirmation":["doesn't match Password"],"full_messages":["Password confirmation doesn't match Password"]}}

メールが送信されていることを確認

f:id:Kenta-s:20191115224246p:plain

"Confirm my account" をクリックするとconfirmされて、devise_token_auth.rbで設定したURL(今回の場合http://localhost:5000/welcome)にリダイレクトされます。

これでアカウント作成は完了です。

ログイン

以下のように正しいemailとpasswordの組をPOSTするとトークンなどの情報が返ってきます

$ curl localhost:3000/auth/sign_in -X POST -d '{"email":"example@example.com", "password":"testpassword"}' -H "content-type:application/json" -i

レスポンス

HTTP/1.1 200 OK
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Download-Options: noopen
X-Permitted-Cross-Domain-Policies: none
Referrer-Policy: strict-origin-when-cross-origin
Content-Type: application/json; charset=utf-8
access-token: Wbu2CnEA1P-B--Ey06YtXg
token-type: Bearer
client: 1JrF3K-KFeFRcY5ase5h6Q
expiry: 1575038375
uid: example@example.com
ETag: W/"0b844d681927a23677ff78329b4c7409"
Cache-Control: max-age=0, private, must-revalidate
X-Request-Id: 23342d72-4dde-4c3c-9397-0a57c053ed19
X-Runtime: 0.177614
Transfer-Encoding: chunked

{"data":{"id":1,"email":"example@example.com","provider":"email","uid":"example@example.com","allow_password_change":false,"name":null,"nickname":null,"image":null}}

このレスポンスに含まれる以下の5つがauthenticationに必要なのでクライアント側でlocalStorageに書き込んでおきます

access-token: Wbu2CnEA1P-B--Ey06YtXg
token-type: Bearer
client: 1JrF3K-KFeFRcY5ase5h6Q
expiry: 1575038375
uid: example@example.com

localStorageに書き込む例

localStorage.setItem("access-token", authToken)
localStorage.setItem("uid", uid)
localStorage.setItem("token-type", "Bearer")
localStorage.setItem("client", client)
localStorage.setItem("expiry", expiry)
認証が必要なエンドポイントを叩く

リクエストヘッダーにこれら(トークンなど)の情報を付与すると、おなじみの current_userauthenticate_user! などが動作します

axiosでリクエストを投げる例

const instance = axios.create({
    headers: {
      "access-token": localStorage.getItem('access-token'),
      "token-type":   "Bearer",
      "client":       localStorage.getItem('client'),
      "expiry":       localStorage.getItem('expiry'),
      "uid":          localStorage.getItem('uid')
    }
})
instance.get(`/api/v1/books`)
  .then(response => 処理)
  .catch(error => 処理)
CORS設定

実際にはcurlではなく別オリジンからリクエストを投げることになるので、config/initializers/cors.rbにcors用の設定を追加します

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do

    # とりあえずifで分岐してますが、env varもしくはoriginをenvironmentごとに管理するconfigファイルを用意したほうが管理しやすくなると思います
    if Rails.env.development?
      origins 'localhost:5000'
    elsif Rails.env.production?
      origins 'example.com'
    end

    resource '*',
      headers: :any,
      expose: ['access-token', 'expiry', 'token-type', 'uid', 'client'],
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

これで許可したオリジンからアカウント作成とログインができるようになります。 トークンなどの情報はレスポンスボディではなくレスポンスヘッダーに含まれているので、見当たらない場合はヘッダーをよく見てください。

以上です。


Omniauthを使う場合は、認証後にリダイレクトさせるときにクライアント側のURLを指定しておいて、クエリストリングについてくるtokenなどの必要な情報をcomponentDidMountなどのタイミングでlocalStorageに書き込むといいです(ツイッターしか試してないですが

omniauthについてはそのうち別記事を書くかもしれません