パスワードを利用せず、メール送信などでログインさせる方法を考えてみた

最近、「パスワードを覚えられない方はこちら」的な機能をぼんやりと考えている。

## 背景と目的

経緯は下記ブログエントリに書いたので時間のある方はどうぞ。 
[JICS 2014のセキュリティ・トラックを聞いて思ったこと](http://d.hatena.ne.jp/ritou/20140119/1390136780) 
パスワードを用いた認証は世の中に広がっているが、複数サービスにおけるパスワード使いまわしが一般的な状況の中、あるサービスからメアド/パスワードの組み合わせなどが漏えいし、それを用いて他のサービスへの不正ログインが発生、問題となっている。他にも細かい話はいろいろあるが、「ユーザーは簡単なパスワードしか設定しないし、使いまわす」というのが現状である。

この問題を受けて、あえてサービスが長いパスワードをユーザーに払い出し、それを利用させるのはどうかという話もあるが、使いまわしを起因とする不正ログインを防ぐことはできてもユーザーの利便性などで課題は残る。
一方、パスワードを記憶できず、毎回パスワードリセット/再設定を行うユーザーがいる。それどころか、あえてパスワードを覚えないユーザーもいると聞く。このようなユーザーに対して、サービス側が長くて複雑なパスワードを払い出す「なんちゃってOTP」的なしくみにすることで使いまわしを防げるのではないか、という話が出てきた。厳密にはワンタイムではなく、次回再設定するまでのパスワードを払い出すというものである。

そんな話の流れの中で、「ユーザーにパスワード設定すら行わず、毎回メールやSMS, Pushのしくみを用いて通知を送り、それを受け取れたらログインできる方法を用意すれば良いのではないか。ちょっと真面目に考えてみよう。」と思い始めた。名前をつけとかないと後で面倒なので、ここでは「デリバリーログイン」とでも呼んでおこう。

ここで「メールのURL送ってクリックしたらログイン完了！簡単だぁぁぁあ」なんて言って終わってもいいのだが、細かい話もあるのでまとめていく。

## 配信方法

デリバリーログインの配信方法としては、次の2つがあるだろう。

* 本人が管理している”メールアドレス宛てにメール送信
* 本人が登録済の端末”にSMSなりpush通知

前者は今までのパスワードリセット/再設定で実績がある。端末登録的なものが安全にできるのならば後者も考えると最近のスマホユーザーが喜びそうである。

本投稿では前者のメールを用いたフローについて検討する。

## 画面遷移

画面遷移の要件については、パスワードリセット/再設定とデリバリーログインで微妙に異なるケースがあるような気がしている。

例として、PC(で閲覧可能な)メアドを登録、PCからサービスを利用しようとしているユーザーのパスワードリセット/再設定フローは次のようになる。

1. PCでサービスにアクセスしたが、パスワードを覚えていない
2. パスワードリセット/再設定のフローに入り、PCメアドにパスワード再設定用URLが送られる
3. PCでメールを確認し、URLからパスワードを再設定をする
4. PCで再設定したパスワードを用いてログイン

3,4は一緒にされるケースも多いのだが、あえて分けている。

デリバリーログインの場合は次のようになる。

1. PCでサービスにアクセスしたが、パスワードが設定されていない
2. デリバリーログインのフローに入り、PCメアドにログイン用URLが送られる
3. PCでメールを確認し、URLからログイン

3ではChromeで別タブでgmailチェックしてるようなイメージ。まぁ、これは特に問題なさそう。

携帯メアドで登録していてPCからサービスを利用しようとしているユーザーのパスワードリセット/再設定フローは次のようになる。

1. PCでサービスにアクセスしたが、パスワードを覚えていない
2. パスワードリセット/再設定のフローに入り、携帯メアドにパスワード再設定用URLが送られる
3. 携帯メールを確認し、URLから端末内のブラウザで再設定をする
4. PCで再設定したパスワードを用いてログイン

デリバリーログインの場合は、3でパスワードを設定させないので、次のようなフローになるだろう。

1. PCでサービスにアクセスしたが、パスワードが設定されていない
2. デリバリーログインのフローに入り、携帯メアドにログイン用URLが送られる(このとき、PCには謎のフォームが出現
3. PCでメールを確認し、URLからログインしようとすると、セッションが違うとか言って簡単な文字列が表示される
4. PCのフォームに3で表示された文字列を入力、ログイン

別に携帯メールじゃなくても、別のブラウザで開くとかも同じ感じになる。

## 必要なロジック

1. GET https://example.org/delivery_auth/request
 * メアドを入力させる
2. POST https://example.org/delivery_auth/request
 * (一般的なCSRFチェック) 
 * メアドを受け取り、登録済かどうかを確認
 * デリバリーログイン用のセッションIDを発行(有効期限短め。HTTP Cookieの有効期限ではなくCookie文字列自体に有効期限を含んで署名しておくとか)
 * 乱数を用いてrequest_idを生成し、メアドと上記セッションIDとともに有効期限付きでkvs的なものに保存
 * https://example.org/delivery_auth/login?request_id=(文字列) のようなURLを含むメールを送信
 * /delivery_auth/finishに遷移して、異なる端末で/delivery_auth/loginにアクセスした時のためのフォームを表示
3. GET https://example.org/delivery_auth/login
 * request_idの値をキーにしてメアドとセッションIDをひく
 * 値が引けたら、request_idをキーにしたデータを削除、引けなかったらエラー
 * 保存されていたセッションIDとブラウザのデリバリーログイン用セッションIDと検証
 * マッチしていたら対象のメアドでログイン処理を実行
 * マッチしていなかったら、もう一回短めの乱数を生成(delivery login PIN)を生成、request_idと紐づくセッションID、メアド、有効期限と紐付けて保存し、表示
4. GET https://example.org/delivery_auth/finish
 * 2の最後で遷移し、フォームが表示されている
 * 3で別の端末で/delivery_auth/loginを開いた時に表示されるdelivery login PINを入力する
5. POST https://example.org/delivery_auth/finish
 * (一般的なCSRFチェック) 
 * delivery login PINが有効かどうかを確認
 * 有効だったらdelivery login PINをキーにして保存されているデータを削除
 * delivery login PINに紐づくセッションIDとマッチしているかを検証
 * マッチしていたら、対象のメアドでログイン処理を実行

ここまでくればデモを実装できそう。気になる点はコメントをいただきたい。