Sansan Builders Box

Sansanのものづくりを支える技術やデザイン、プロダクトマネジメントの情報を発信

Eightを例にした、やさしい二段階認証の話

初めまして。Eight事業部のPlatform Unit エンジニアの 李です。

私が属するPlatform Unitは基盤チームとも呼ばれ、Eightをより安心して使えるサービスにするための開発を行っています。 私は主にセキュリティー周りの開発を担当しています。今回は5月末にリリースしたEightの二段階認証機能について、調査や開発をする際にあった学びや気づきを共有できればと思います。

2段階認証機能を開発するにあたって

5月末にEightに二段階認証がリリースされました! これで前より安心して使えるサービスになりましたが、これからの内容は主に調査や開発をする際にあった学びや気づきです。

私は業務上で利用しているサービスではすでに二段階認証を設定していました。と言ったものの中で実際にどんなことが起こっているのかわからず、どこから手を入れればいいのかが全く不明でした。 早速調べてみるとWikipediaさんはこう言っています。

多要素認証は、アクセス権を得るのに必要な本人確認のための要素を複数、ユーザーに要求する認証方式である。 必要な要素が二つの場合は、二要素認証や二段階認証とも呼ばれる。

なるほど。ここで気になる一点 二要素認証二段階認証 はなんの違いがあるのでしょう?

二要素認証

世の中で使用されている認証はその方法も山ほどありますが、大きくは三つの要素に分けられます。

  • PWD、PINコードのような ユーザーだけが知る情報
  • 鍵、クレジットカードのような ユーザーが持っているもの
  • 指紋、虹彩のような ユーザーの生体情報

ID/PWDを使った認証はほとんどのサービスで行われていて、ここでもう一つの要素を加えたのが 二要素認証 になります。

身近な例で例えると、自宅の金庫に貴重品があるとして、鍵を持った人が家には入れても、暗証番号がついている金庫は開けられない。逆に金庫の暗証番号がわかる人でも、鍵がないとそもそも家に入れない。のようなことですかね。

二段階認証

二段階認証は、認証要素は問わず段階を分けて認証を行うことを言います。

長く使われなかったサービスにログインした時に、生年月日、自分が設定した質問などを問われた経験はありませんか?そういったものが一つの例になります。

ID/PWDも質問の答えも ユーザーだけが知る情報 といった性質は同じですが、それを二段階に分けて求めていて 二段階認証 と呼ぶのですね。

二要素を段階的に求めれば、二段階かつ二要素認証である、と整理しても良さそうです。 また二要素であることが、よりセキュリティ的に強固なのは明白なことでしょう。

インタネットサービスでの二段階認証

インタネットサービスの二段階認証は、なんらかの方法で発行したワンタイムパスワードを使うような方法が多く使われています。 そのなんらかの方法には TOTPHOTP というものがあります。

時刻ベースのワンタイムパスワード TOTP: Time Base One Time Password

TOTPはサーバーとクライアントが同じシークレット情報を持った上で、シークレット情報と時間情報の組み合わせでワンタイムパスワードを発行する方法です。

実装

rubyでは rotp というgemを使うと簡単に実装できます。

require 'rotp'
require 'rqrcode'

class TotpAuthenticator
  def initialize
    @secret_key = ::ROTP::Base32.random
  end

  def qr_code
    RQRCode::QRCode.new(provisioning_uri,
                        size: 12)
  end

  def valid_otp?(otp)
    !otp_generato.verify(otp,
                         drift_ahead: 30, # 30秒前のOTPも考慮する
                         drift_behind: 30, # 30秒後のOTPも考慮する
                         at: Time.now).nil?
  end

  private

  attr_reader :secret_key

  def otp_generator
    @otp_generator ||= ROTP::TOTP.new(secret_key,
                                      issuer: 'issuer',
                                      interval: 30) # 30秒ごとにOTPを更新する
  end

  def provisioning_uri()
    otp_generator.provisioning_uri('test@example.com')
  end
end

流れ

二段階認証の設定プロセスは簡単にこうなります。

  • サーバーは二段階認証のためのシークレットを発行する
  • そしてそのシークレットを元に認証アプリ登録用のURIを生成する
  • URIをユーザーが簡単に読み取れる形式にして(ex: QRコード)ユーザーに伝える
  • ユーザーはその情報を認証アプリに読み込み、サービスを登録する

また、登録後の認証では

  • ユーザーは通常の方法で認証を試みる
  • サーバーはユーザーを確認し、ワンタイムパスワードを問う
  • ユーザーは認証アプリに表示されたワンタイムパスワードを入力する
  • サーバーはシークレットと 現状時刻*1を元にワンタイムパスワードを発行する
  • ユーザーのワンタイムパスワードと比較し、認証を行う

どうですか?意外とシンプルですよね。 いろんなサービスで二段階認証を使われているユーザーさんには馴染みのある認証方法だと思います。

カウンターベースのワンタイムパスワード HOTP: HMAC-based One-time Password

HOTPも同じくサーバーとクライアントが同じシークレット情報を持った上、シークレット情報とパスワードの生成回数の組み合わせでワンタイムパスワードを発行する方法です。 サーバーでは認証するたびに生成回数が増加し、それに合わせてユーザーもトークンをリフレッシュしなければなりません。 カウントのずれが発生した場合には同期作業が必要になります。 サービスとしては SMS認証 で利用されることが多いです。

実装

TOTPとほぼ似ていますが、サーバーは生成回数の情報を持ってなければいけません。

require 'rotp'

class HotpAuthenticator
  def initialize
    @secret_key = ::ROTP::Base32.random
    @count = 0
  end

  def count_up
    self.count = self.count + 1
  end

  def valid_otp?(otp)
    !otp_generator.verify(otp, count).nil?
  end

  attr_accessor :secret_key, :count

  private

  def otp_generator
    @otp_generator ||= ROTP::HOTP.new(secret_key)
  end

  def provisioning_uri
    otp_generator.provisioning_uri('test@example.com')
  end

  def generate_otp
    otp_generator.at(count)
  end
end

流れ

登録プロセスはTOTPとそこまで変わりはありません。

ただ認証時には、ユーザーのワンタイムパスワードは自動更新されないという違いがあります。

  • ユーザーは通常の方法で認証を試みる
  • サーバーはユーザーの確認後、ワンタイムパスワードを問う
  • ユーザーは認証アプリで取得したワンタイムパスワードを入力する
  • サーバーはシークレットと生成回数を元にワンタイムパスワードを生成する
  • ユーザーのワンタイムパスワードと比較して、認証を行う
  • サーバーはパスワードの生成回数をカウントアップする
  • ユーザーは認証アプリでワンタイムパスワードを再発行(カウントアップ)する

Eightの二段階認証

EightではTOTP方式の 認証アプリを利用する 方法と、 HOTP方式の SMS認証で受け取る 方法を用意しています!

f:id:mamaiika:20190703234810p:plain

結論

色々話しましたが実は二段階認証の設定には1分もかかりません。より安全にEightを使ってみませんか?

関連リンク

*1:RFC6238 にもありますがデバイスとサーバー間での時刻ずれやユーザビリティを考慮し、前後の時刻を許容するケースもあります。セキュリティとのバランスを考慮し設定すると良さそうです。

© Sansan, Inc.