Edited at

二段階認証(Two-Factor Auth)の実装

More than 5 years have passed since last update.

ふと思い立って、GmailにTwo-Facotr Auth(二段階認証/二要素認証)をかけてみました。次にEvernoteにも設定した時に、Google謹製のアプリでEvernote用のワンタイムパスワードを発行できることに疑問を感じました。

ログイン時に「一定時間有効なIDを発行する」のはサーバー上で逐次生成してExpireを設定すれば可能ですが、他社でも共通になる仕組みはどのようになっているのかと思って調べてみたらなかなか面白かったでメモしておきます。


仕組み

実はRFCで規定されているアルゴリズムを使ったオープン実装でした。

http://ja.wikipedia.org/wiki/Google_Authenticator


  • サーバー上で秘密鍵を生成し、ローカルとサーバーに保存する

  • この秘密鍵と時間を元にして30秒間有効なワンタイムパスワードを生成する

  • サーバー上でも同じアルゴリズムを使って照合する

ここで生成する秘密鍵はユニークに生成するのは当然。秘密鍵をサーバー所に保存せずユーザー情報のHash等にすると、漏洩した時に他の人のキーも推測されてしまいますし、リセットもできなくなります。

アプリ(ローカル)にはこの秘密鍵をBase64した16文字のキーを渡しておきます。アプリ側ではユーザーには秘密鍵は参照できないようにしておきます(再登録等の場合は再生成でセキュリティを確保)


実装

詳しい解説はここにありました

http://yamatamemo.blogspot.jp/2011/06/oath-3.html

と思ったらちゃんと実装してGem化してくれていました。すばらしい。

Ruby One Time Password libraryというものがあるようなので使ってみます。


インストール


~ gem intall rotp



使い方

下記のように秘密鍵を与えてnewしてからnowを呼ぶとワンタイムパスワードが返ってきます。

totp = ROTP::TOTP.new("base32secret3232")

totp.now # 例:=> 492039

# 上のコードでチェックを行う(30秒有効)
totp.verify(492039) # => true
sleep 30
totp.verify(492039) # => false

もちろん、この時の秘密鍵が同じであれば同じワンタイムパスワードになるので2段階認証をONにするタイミングで下記のようにしてユニークなキーを作成して保存しておきます。

ROTP::Base32.random_base32 # 例:=> "5lrymooffwxl4ltq"

後は、ログイン時に秘密鍵があれば、ワンタイムパスワードを生成してTwillio等を利用してSMSに送信してコード入力を促す部分を作ってVerifyすればOKです。


ぶつぶつ

モバイルデバイス必須ですが、難しい事を考えずにRubyや他の言語でも簡単に実装できます。

有効期限30秒以下ってSMSの通知速度や時刻ズレを考えるとかなり厳しいような気がしますが、サーバーとスマホは時刻同期されているし、ログインしようとしてページを開いている時の通知なのでそれでも十分なのですね。

ワンタイムパスワードってキーホルダー型トークンとかトークン発生サーバーとか必要なイメージがあったのですが、これならうちの業務サイトでも実装できそうだな...と思いました。