devise_token_authを使った認証が自分にとって定番パターンになりつつあるので手順を残しておきます。
RailsはAPIモードで、クライアントは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が送信されない問題の対策です。そのうち不要になるかもしれないです。
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"]}}
メールが送信されていることを確認
"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_user
や authenticate_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についてはそのうち別記事を書くかもしれません