valid,invalid

関心を持てる事柄について

Rails で Slack のような Magic Link による認証を実装する

passwordless という gem が最近リリースされたようなので少し触ってみた。

名前の通り認証時にパスワードを必要とせず、いわゆる Magic Link によるログイン機構を Rails アプリケーションで実現できる。

Magic Link とは

Slack や Medium が実装しているこの機能は「リンクを知ることのできる立場にあれば本人であろう」という二要素認証に似たアイデアに基づいている。

ユーザーがパスワードを覚える必要がないというUX観点だけでなく、パスワードによる認証の弱点とされる推測やブルートフォース攻撃による不正アクセスや乗っ取りのリスクを低減できるメリットがあるとされている。

japanese.engadget.com

postd.cc

とにかく試す

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つは特に気にしなくてよい。

特筆するのは passwordless_sessions だ。

このテーブルは PasswordlessSessions class に結びついている。bin/rails passwordless:install:migrations コマンドで生成される、文字通りセッションを管理する。格納されるデータがどのようなものかについては後述する。

Magic Link の挙動

ユーザー作成

まずログインするとこんな感じの画面が見える。nil の部分にはログイン状態であればユーザー情報が表示される。

f:id:ohbarye:20171220005752j:plain

Sign in できるユーザーが存在しないので New User から sign up する。

f:id:ohbarye:20171220005804j:plain

Sign up すると create したユーザーで sign in した状態になる。Create 直後に呼んでいる Passwordless::ControllerHelpers#sign_in を見るとわかるように、単に cookie に token をセットしているだけ。

f:id:ohbarye:20171220005831j:plain

この時の 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 ページへ。

f:id:ohbarye:20171220005852j:plain

Send magic link を押すとメールが送信される。

f:id:ohbarye:20171220005927j:plain

サーバー側では 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 にアクセスしてみるとログインした状態でページが表示された!!

f:id:ohbarye:20171220005948j:plain

Sign out して再度同じ URL にアクセスするとセッション期限切れとしてきちんとエラーになる!!

f:id:ohbarye:20171220005957j:plain

routes

上記users/sign_in のような URL はどのように生成されるのだろか?

使用者は routes.rbpasswordless_for を書く。PasswordlessRouterHelpers.passwordless_forPasswordless::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 時点ではまだリリースされていないようだ。

github.com

この他にもいくつか実装サンプルや野良 gem を見つけたがデファクトぽいものは見つからなかったのでもしかしたら passwordless は流行るかもしれない。名前もわかりやすいし。