passwordless という gem が最近リリースされたようなので少し触ってみた。
名前の通り認証時にパスワードを必要とせず、いわゆる Magic Link によるログイン機構を Rails アプリケーションで実現できる。
Magic Link とは
Slack や Medium が実装しているこの機能は「リンクを知ることのできる立場にあれば本人であろう」という二要素認証に似たアイデアに基づいている。
ユーザーがパスワードを覚える必要がないというUX観点だけでなく、パスワードによる認証の弱点とされる推測やブルートフォース攻撃による不正アクセスや乗っ取りのリスクを低減できるメリットがあるとされている。
とにかく試す
test/dummy
ディレクトリにダミーの Rails アプリが実装されているので clone して起動するだけで試すことができる。今回の記事はこれを読みつついくつかコマンドを叩いただけにすぎない。
git clone git@github.com:mikker/passwordless.git cd passwordless bundle bin/rails db:migrate bin/rails s
DBの変更
db:migrate
で作られるテーブルたちを見てみる。
rb(main):001:0> ActiveRecord::Base.connection.tables => ["schema_migrations", "ar_internal_metadata", "passwordless_sessions", "users"]
以下の3つは特に気にしなくてよい。
users
はUser
model に対応するテーブルschema_migrations
は migration 情報を管理するテーブルar_internal_metadata
は Rails5 から追加されたはDB誤爆削除防止機能用のテーブルらしい
特筆するのは passwordless_sessions
だ。
このテーブルは PasswordlessSessions class に結びついている。bin/rails passwordless:install:migrations
コマンドで生成される、文字通りセッションを管理する。格納されるデータがどのようなものかについては後述する。
Magic Link の挙動
ユーザー作成
まずログインするとこんな感じの画面が見える。nil
の部分にはログイン状態であればユーザー情報が表示される。
Sign in できるユーザーが存在しないので New User から sign up する。
Sign up すると create したユーザーで sign in した状態になる。Create 直後に呼んでいる Passwordless::ControllerHelpers#sign_in を見るとわかるように、単に cookie に token をセットしているだけ。
この時の response を覗いてみると Set-Cookie
header が付いているのがわかる。
Set-Cookie: user_id=SjU3d1FBZ04vOXc1dTB3c1lIbnRHUT09LS1BbW5KczczNkRQbUd6MzBzUU5XRGx3PT0%3D--0f10eb4a45ebf818056d04ffb90b5659ac4dce08; path=/; expires=Sat, 19 Dec 2037 14:44:23 -0000 Set-Cookie: _dummy_session=dHNqVzlqakt5K1VSc0d5VE1aLzR0SUVYRnM4NVlUQnRhSW9CNXI0NnMyMnhKby95L3VJTERZN1ZyT3RnVFNmZkI2NWViN2NjYm8wS3UrSlN6eWwvaW93WGRENDF3MHN3REdqRjY4SzFBYVpZOWxQYmpmQmpxMkNYTVpROEdIL1JIYkVLNzRheXJmOHN3T0g3RUJneVZNd1FsUXNXeC9QQloxZ1ZhM3JzOHUrOGtleThTSXlzaytkOXl3MTdUVjhNK2Z6aWZpOTZiQUVBaTA4STZ2LzlHc0llWGVaWmpvWVE0RjlTdm54elBRN1QxU21Pc3NEL3NqQUlaRVlkZE1QVC0tNENDRW12UTJwY2xrY0Y2WHRQempaQT09--13e0a4893161060d7fee5357d403eb407efc3b12; path=/; HttpOnly
Magic Link
ユーザーができたので sign out して sign in ページへ。
Send magic link
を押すとメールが送信される。
サーバー側では Passwordless::Session
が create されている。データを見るとわかる通りセッションの有効期限も自動的にセットされる。デフォルトで1年後になっているがこのあたりは後々カスタマイズ可能になるかもしれない(オープンクラスなどでできなくもないが避けるのが無難そう)。
irb(main):013:0> pp Passwordless::Session.last; nil #<Passwordless::Session:0x00007fe85dea6508 id: 1, authenticatable_type: "User", authenticatable_id: 2, timeout_at: Tue, 19 Dec 2017 15:52:57 UTC +00:00, expires_at: Wed, 19 Dec 2018 14:52:57 UTC +00:00, user_agent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.84 Safari/537.36", remote_addr: "::1", token: "PR5h4ZHwAHIidrsMGFyU91ji8KwCgfSZ9PBhOvMQjPo", created_at: Tue, 19 Dec 2017 14:52:57 UTC +00:00, updated_at: Tue, 19 Dec 2017 14:52:57 UTC +00:00>
Magic Link の在り処はどこだろうか。letter_opener や mailcatcher のような便利gemは bundle されていないので console を確認する。Magic Link が記載されたメールがログに出力されている。
Passwordless::Mailer#magic_link: processed outbound mail in 28.8ms Sent mail to over.rye@gmail.com (9.3ms) Date: Tue, 19 Dec 2017 23:52:57 +0900 From: CHANGE_ME@example.com To: over.rye@gmail.com Message-ID: <5a3927c99da8f_140273fc69a1ca14c261a@ohbas-MacBook-Pro.local.mail> Subject: =?UTF-8?Q?Your_magic_link_=E2=9C=A8?= Mime-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Here's your link: http://localhost:3000/users/sign_in/PR5h4ZHwAHIidrsMGFyU91ji8KwCgfSZ9PBhOvMQjPo
http://localhost:3000/users/sign_in/PR5h4ZHwAHIidrsMGFyU91ji8KwCgfSZ9PBhOvMQjPo
にアクセスしてみるとログインした状態でページが表示された!!
Sign out して再度同じ URL にアクセスするとセッション期限切れとしてきちんとエラーになる!!
routes
上記の users/sign_in
のような URL はどのように生成されるのだろか?
使用者は routes.rb
に passwordless_for
を書く。PasswordlessRouterHelpers.passwordless_for
で Passwordless::Engine
を mount していることがわかる。
Rails.application.routes.draw do passwordless_for :users resources :users # others end
routes 一覧を表示してみると Routes for Passwordless::Engine:
の直下に生成された routes が見える。
$ bin/rails routes Prefix Verb URI Pattern Controller#Action users /users Passwordless::Engine {:authenticatable=>"user"} GET /users(.:format) users#index POST /users(.:format) users#create new_user GET /users/new(.:format) users#new edit_user GET /users/:id/edit(.:format) users#edit user GET /users/:id(.:format) users#show PATCH /users/:id(.:format) users#update PUT /users/:id(.:format) users#update DELETE /users/:id(.:format) users#destroy registrations POST /registrations(.:format) registrations#create new_registration GET /registrations/new(.:format) registrations#new secret GET /secret(.:format) secrets#index root GET / users#index Routes for Passwordless::Engine: sign_in GET /sign_in(.:format) passwordless/sessions#new POST /sign_in(.:format) passwordless/sessions#create token_sign_in GET /sign_in/:token(.:format) passwordless/sessions#show sign_out GET|DELETE /sign_out(.:format) passwordless/sessions#destroy
他の gem は?
つい最近 Sorcery でも同様の機能が実装されたようだが 2017/12/20 時点ではまだリリースされていないようだ。
この他にもいくつか実装サンプルや野良 gem を見つけたがデファクトぽいものは見つからなかったのでもしかしたら passwordless は流行るかもしれない。名前もわかりやすいし。