カラーミーショップ セキュリティ

カラーミーショップにSMSを用いた2要素認証を実装しました

カラーミーショップ セキュリティ

カラーミーショップでは、ショップオーナーが利用する管理者ページに管理者が2要素認証でログインできる機能を2020年3月16日にリリースしました。この記事では、いかにしてカラーミーショップの管理者ページに2要素認証を実装していったかについて、担当したデザイナーとエンジニアから紹介します。

以降の文中では、カラーミーショップの管理者ページのことを、そのサブドメインからadminと呼び、2要素認証のことを2FAと呼びます。

2FA機能の概要

今回はSMSを用いた2FA機能をカラーミーショップのadminに実装しました。次の手順で2FAを設定できます。

  1. adminログイン後に「オーナー情報」ページから「2要素認証」へ進む
  2. 画面の案内にしたがってSMSを受信できる携帯電話番号を入力し、認証コードを記載したSMSを受信する
  3. 認証コードを画面の案内にしたがって入力することで端末の所持者であることを認証し、設定を完了する

ログイン時は次の手順で認証します。

  1. ログインページでログインID(もしくはショップURL)とパスワードを入力後、認証コードを記載したSMSを受信する
  2. その認証コードを画面へ入力する

また、リカバリーコード機能もサポートしています。2FAの設定画面で表示できる10件のリカバリーコードをあらかじめ安全な場所に保存しておくことで、万が一端末にアクセスできなくなっても、認証コードの代わりにリカバリーコードでadminにログインできます。1件のリカバリーコードの使用は1回限りです。

2FAを実現するアーキテクチャ

2FA機能は、カラーミーショップを構成するロールのうちの二つと、SMSを送信するための外部サービスを組み合わせて実現しています。

  • ショップオーナーが利用するadmin
    • 管理者ページ全体を担当するPHPアプリケーション
    • 2005年のサービスローンチ時から受け継がれてきた15年の歴史を持ち、主要なコードだけで370,000行以上ある大規模なコードベース
  • adminから利用するAPI
    • プライベートなWeb APIを担当するRailsアプリケーション
    • 2FAに関係しないビジネスロジックのモデルはすでに揃っている
  • APIがSMSを送信するために利用するTwilio

今回のアーキテクチャが実現する処理はシーケンス図で次のように表現できます。

admin、API、Twilioを組み合わせて2FAを実現しています

実装にあたっての技術的な課題と解決策

新たにadminへ2FAを実装するにあたって、「adminにおける既存のログイン処理が複雑で手を入れづらい」という課題が存在しました。具体的には次のような課題がありました。

  • フレームワークを使わずに構築されたアプリケーションであり、認証やセッション管理が長年の運用を経て密結合している
  • 管理者と副管理者の認証処理がバラバラにベタ書きされている
  • テストがない
  • 結果として2FAに関する処理を入れる余地がない

ここからは、これらの課題を解決しつつ、各ロールにどのように2FA機能を実装していったかについて説明します。

adminの実装

前述した課題に対応するべく、現状のログイン処理の整理からすることにしました。既存の実装に対するテストは存在しておらず、挙動に変わりが無いことを担保するためにもそこから始めようと考えました。

ログインという1つの処理の中に、reCAPTCHA認証、ユーザー情報の取得、パスワードの確認といった処理が含まれ巨大な関数になっていました。加えて、グローバル変数である$_SESSION により値の受け渡しが行われているため、処理自体の入出力が分かりづらいというのが現状でした。 この状態の処理に対しテストケースを用意するのが大変だったため、テストは手動をメインにはじめることにしました。

方針は以下の通りです。

  • 認証処理とセッション操作を別々のクラス・関数として抜き出し、単体でのテストを簡単にする
  • 散らばった管理者・副管理者の処理の入り口をまとめ、処理内部で分岐する
  • 読み解けない処理は極力そのままに

一つの処理にベタっと書かれた処理に名前をつけてあげることで可読性があがった、と感じています。以下のシーケンス図は今回の対応の前後を表現しています。

認証処理のリファクタリング

今回現状維持した処理に関しても今後良くしていきたいと考えています。

APIの実装

ロジックはできるだけAPIに集約する

adminに古くから存在するロジックの多くでデータベースの読み書きを実行しています。しかし、今回APIをアーキテクチャに組み込んだことからわかるように、設定の登録や認証状態の記録、またSMSの送信など、2FAに関するロジックのほとんどをAPIに集約しました。理由は次のとおりです。

  1. すでにAPIを担当するRailsアプリケーションがモデルを持っているから
  2. 2FAに関する機能をadminから使うとき、インタフェースに依存させたいから

まず1.については、APIを担当するRailsアプリケーションは、従来からカラーミーショップのビジネスロジックに関するモデルを豊富に持っていました。極力これらのモデルを利用しながらAPIエンドポイントを実装することで、admin単体で実装していく場合と比較した開発スピードの向上を狙いました。

また、2.については、重要なロジックをAPIに集約することで、adminの2FAに関する機能はAPIの提供するインタフェースに依存するようになります。これによって、admin側の実装はAPI呼び出しに応じて画面をレンダリングする処理が中心になるので、コードの見通しがよくなるという効果が得られます。また、インタフェースにだけ依存されているAPI側にとっても将来的な実装変更が容易になります。

拡張を見越したテーブル設計にする

今回の実装では、SMSによる認証コードの送信を認証方法としてサポートしています。今後、TOTPなど他の認証方法をサポートする場合に備えることと、テーブルの性質上そこまでレコード数が膨らむことはない(高々これまでのアカウント数の合計)ことを鑑み、カラムの重複が少なくなることを優先して、2FAの設定に関するテーブルはクラステーブル継承(CTI)に則った設計にしています。

CTIに則った設計にしています

SMS送信は非同期ジョブで実現する

TwilioのSMS APIを用いたSMS送信は、ある程度時間がかかる外部API呼び出しであることや、万が一通信エラーが発生したときのリトライのことを考えて、Sidekiqを使った非同期ジョブで実現しています。すでにカラーミーショップへの新規申込時にSMS認証が存在しますが、それと同じ方法です。

このAPIが発行する非同期処理を担うSidekiqは、今回の2FA用のSMS送信を含め、複数の用途のための複数のキューを持っています。SMSの送信ジョブを持つキューの優先度を高く設定することで、実際に認証するときにSMSがなかなか送られてこない状況を避けるようにしています。APIを担当するRailsアプリケーションのconfig/sidekiq.ymlに書く設定のイメージを次に示します。

:queues:
  - [signup_sms, 3]
  - [2fa_sms, 3]
  # 優先度3未満の他のキューの設定...

2FA画面のデザイン

2FAは、カラーミーショップがより大規模なネットショップを受け入れて行く戦略において、よりセキュリティを高め、安心してショップ運営を行いたいという、ショップオーナーの声に答えた施策です。なるべく簡単に正確に2FA設定を行っていただくため、以下のようなHCDサイクルを通して、画面デザインを行いました。

ユーザー要件定義

まず、この「2要素認証(2FA)」において、ターゲットユーザー、およびユーザーが叶えたい要件、それに対するペインを整理しました。

画面デザインの仮実装

  • 通常フロー : 「ヤコブニールセンの10か条」をベースにしたユーザビリティチェック項目に従い、エンジニアが作成したダーティプロトタイプに対し、ボタンや説明文言の調整を行いました。特に、ユーザーが「現在のステータスがわかること」「ボタンを押した後に何が起こるかわかること(予期的UX)」については重点的に調整を行いました。
  • エラーフロー : 上記の通常フローだけではなく、エラー文言、およびエラー時のユーザータスクの案内についても、上記のユーザビリティチェック項目に従ってデザインの仮実装を行いました。

ユーザーテスト用の仮説

上記で画面デザインを仮実装し、プロトタイプができたので、デザイナーのバイアスを取り除くため、ユーザーテストを設計する必要がありました。最初に定義したユーザー要件に照らし合わせ、画面仮実装時の調整が真に効果があるかの仮説を立てました。

ユーザーテスト設計

上記の仮説を元に、ユーザー役を社内でリクルーティングし、テスト項目を決めました。

パイロットテスト

今回は在宅勤務期間だったこともあり、オンラインでのユーザーテストが求められました。そのため、Zoomを用いたセッティングの準備を行い、弊社の @keitakawamoto に協力を仰ぎつつ、パイロットテストを行いました。そこで出た改善点をもとに、テスト設計を見直しました。

オンラインユーザーテストの準備と実施

リクルーティングした社内のユーザー役に対し、事前に必要な情報を与え、オンラインユーザーテストを行いました。テストはZoomの録画機能を用いて保存し、ユーザー役の発話・表情をログとして残しました。

ユーザーテスト結果の共有

上記のユーザーテスト結果について、優先的に対応する事柄を決め、Issueにて報告をしました。

ユーザーテスト結果の結果を画面デザインに反映

テスト結果を元に、リリース時に解決するべき項目を画面デザインに反映しました。例として、ボタンを押した先が想像しにくい箇所のボタン文言を調整したことや、2FA設定の完了までのステップ数が多かった箇所を統合したことなどがあります。

リリース上の工夫

今回は、簡単なフィーチャートグルを使ってリリース前の本番環境での動作確認やユーザーテストを楽に行えるようにしました。

adminでは多くのコードの書き換えや追加が発生することが予想されたので、各変更をどうリリースしていくかについて考える必要がありました。Gitのリリース用ブランチを用意して、機能のリリースまでそのブランチに変更をためこんでいき、ブランチをmasterに追従させ続けるという方法も考えられます。しかし、その方法だとブランチの維持作業にコストがかかることと、今回は既存コードの整理を伴う開発であったことから、2FAに関して入れた変更はつねにすぐmasterにマージするように進めました。具体的には、2FAに関するコードの変更を日常的にmasterにマージしながらも、機能の本番公開まではログイン中のアカウントの権限を確認し、一般のアカウントでは2FAに関連するページにアクセスできないように、フィーチャートグルとなる分岐を入れて制御していました。

逆に、ペパボのパートナー(従業員)のアカウントだけに開発途中の状態も含め2FA機能を随時公開していました。これによって私たち開発者が本番でどんどん挙動を確認することができました。また、ユーザーテストの実施も簡単で、改善点の洗い出しが楽になりました。ユーザーテスト被験者以外の興味のある人にも使ってもらうことで、意見を出してもらうこともできます。さらには、QA時に特別なブランチや設定を用いてQA環境を準備するというような手順は必要なく、単純にmasterをデプロイするだけでいいというメリットもありました。

リリースまでは社内限定で公開というラベルを付与していました

最終的に2FA機能への導線をショップオーナーに見せるのはadminの責務になります。ですので、本番に公開するためのブランチでは、2FA機能の公開に関する権限制御を取り除く変更を入れるだけで済みました。これは万が一の切り戻しも容易という安心感につながりました。

まとめ

この記事では、カラーミーショップの管理者ページ(admin)に2要素認証機能をどのように実装したかについて紹介しました。カラーミーショップの開発チームでは、今後もサービスの改善を継続し、よりセキュアにしていくことで、ショップオーナーの皆様が安心して商いに取り組めるようにしていきます。