ユビーにパスキーを導入しました
はじめに
Ubie プロダクトプラットフォーム所属の nerocrux です。
Ubie にパスキー認証を導入しました。導入してからしばらく時間が経ちましたが、年末になると色々振り返りたくなるので、パスキーを導入した背景や導入方法などを簡単にまとめたいと思います。
導入背景
Ubie では、症状検索エンジン「ユビー」(以下、ユビー
)という一般生活者向けのサービスを展開しています。ユーザーが簡単な質問を回答することで、関連する病名や、適切な受診先情報を得ることができるサービスとなっています。
ユビーを利用する際に、ユーザーはアカウントを登録することができます。アカウント登録の手段として、いわゆるソーシャルログインを利用するほか、メールアドレスを入力したあと、メールアドレスに送られた 6 桁の OTP (有効期限の短い 1 回きりのパスコード) をユビーに入力することで、メールアドレスを所有することを証明して、本人認証を行う手段が以前から実装されていました。
しかしこの認証手段にはいくつかの問題点があります。
- フィッシング耐性がない
メールアドレスと OTP による認証では、フィッシング耐性がありません。
攻撃者が偽のログインページをユーザーに表示しつつ、ユーザーがメールアドレスを偽サイトに送信したら、攻撃者がユビーにそのメールアドレスを使って認証を行います。そうするとユーザーのメールボックスにユビーから OTP が送られるので、この OTP を偽サイトにユーザーが入力してしまうと、攻撃者がそのコードを使ってユビーにログインできてしまいます。
- 認証のサービスレベルがメールサービス提供者に依存する
電子メールを利用して OTP の送受信が行われるので、OTP を含む電子メールがちゃんとユーザーに届くかどうかはメールサービスのプロバイダに依存します。具体的には、以下の要素になります。
- ユビーがメール送信時に利用する SaaS の SLO
- ユーザーがメール受信用のメールサービスのサービス提供状況
メール送信 SaaS はあまり障害が起きる印象がなく安定しているし、料金的にも SMS に比べると安価だと考えますが、急激に送信数が増えてしまうと送信キューが詰まってしまうので、設定を工夫したり(認証 OTP 送付用メールとお知らせなどのメールのキューを分けるなど)、遅延状況に注意を払う必要があります。
- ユーザー体験がよくない
認証を行うため、ユーザーは電子メールを受信し、OTP をユビーのログインページに入力しなければなりません。メールが届くまでの待ち時間、及び数字の記憶・入力もしくはコピーアンドペーストの手間がユーザーにストレスを与えます。
ユビーのアカウントに、ユーザーの症状検索履歴を含めた個人情報が紐づけており、攻撃者の狙い対象になりうるのです。そのため、フィッシング耐性のない Email OTP による認証をフィッシング耐性のあるパスキーで少しずつ置き換えていきたいと考えています。
また、以前の記事でも紹介したとおり、ユビーでは認証体験の改善を継続的に行っています。パスキー認証は外部サービスに依存せず、意図通りに動作した際の UI / UX が非常にシンプルでスムーズですので、積極的にユーザーに認知・体験してもらいたいと考えています。
導入の考え方
パスキーの仕組みを簡単に説明しますと、ユーザーが所持する端末(iPhone, Android, PC など)側で鍵ペアを生成し、秘密鍵を端末のパスワードマネージャーに、公開鍵をユーザーが利用するアプリのサーバー側に保存します。アプリを認証する際に、ユーザー端末側で本人認証(FaceID、Windows Hello など)を行った上、端末がサーバーから受信したメッセージを秘密鍵で署名し、その署名をサーバーが検証することで、サーバー側で認証を行います。この仕組みが成立するには、ペアとなる秘密鍵と公開鍵が、ユーザー端末とアプリサーバーにそれぞれ正しく登録されていることが必要です。
また、パスキーの大きい特徴の一つは、同じベンダー製の複数端末間で秘密鍵が同期できる点です[1]。例えば iPhone 上で作成したパスキーの秘密鍵は、iCloud 経由で同じ Apple ID でログインしている Mac や iPad に同期され、利用可能になります。これはユーザーにとって利便性の高い仕組みといえます。なぜならば、新しい iPhone に買い替えたときなど、Apple ID さえあれば、パスキーを再設定する必要がなく、今まで通りに既存のパスキーでサイトにログインできてしまうからです。
しかし日常生活の中でユーザーが利用するすべての端末が全部同じベンダーのものとは限りません。異なるベンダーへ機種変更したら、パスキーを失くしてしまう可能性もあれば、人為的に端末側もしくはサーバー側のパスキーを誤って削除してしまい、ログインできなくなる可能性もあります。
パスキーは新しい技術ですし、仕様・仕組みも複雑で難しいです。一般生活者に難しい仕様をインプットして、短期間でその特性を理解した上使いこなすことを期待するよりは、まずはその利便性を体験してもらい、少しずつ慣れてもらうことが重要だと考えます。
このような背景があって、現時点ユビーではユーザーにパスキーの作成・利用を強制していません。ユーザーが何かしらの理由でパスキーでログインできなかった場合では、今まで通りに Email OTP で認証可能です。強制しないものの、パスキーの利用を促すキャンペーンの UI を表示して、積極的に体験してもらいたい考えです。
ただし、パスキーの導入は「体験」「選択的利用可能」で終わりではありません。ビジネスの発展とともに、フィッシングや不正ログインのリスク、パスキー認証の成功率や普及度、またはパスキーの仕様と各ベンダーの実装状況をみながら、適切なタイミングで「Email OTP による認証を無効化にするオプション」や「認証手段をパスキーのみに制限するオプション」などをユーザーに開放することを検討します。
パスキーの登録
ユビーでは、パスキーを登録するパスは概ね 2 通りあります。
一つ目は、Email でアカウントを作成するユーザー向けのパスキー登録キャンペーン
の動線です。
ユーザーが Email OTP で認証した後に、「パスキーを登録するか」を尋ねるキャンペーン画面が表示されます。この画面からパスキーを作成できますし、スキップすることも可能です。認証画面は症状検索結果が閲覧できる前の画面で表示されることが多いため、ユーザーは基本的に急いで認証を済ませて結果画面に戻りたいので、この画面はできるだけシンプルにして、症状検索の完走率を毀損しないようにしています。
二つ目は、アカウント設定
画面のパスキー管理セクションからです。
ユーザーが偶然に「アカウント設定」画面を訪ね、パスキーを登録する可能性が低く、何かしらの方法で「ユビーにパスキーが利用可能」「利用すると認証体験が良くなる」ことをユーザーに伝える必要があります。そのため、パスキー未登録のユーザーを対象に、ユビーの設定メニューの下部にパスキー登録を促すバナーを設置しました。バナーをタップすることで、パスキー登録画面に遷移します。
受け入れるパスキーについて
ユビーでは、一つの端末から一つのパスキーしか作成できないように制限しています(excludeCredentials オプション利用)。これは不必要に重複の鍵を作れてしまうと、ユーザーがパスキーを適切に管理できなくなるリスクが高くなるからです。
複数の端末を持ち、一つのユビーアカウントにログインしたユーザーもいますので、それぞれの端末からパスキーを登録できます。
また、一般生活者がよく利用する、メジャー OS / 端末ベンダー(Apple, Google, Microsoft)が作成するパスキーに関しては、クラウド側で同期するものや、Attestation がついていないものも基本的に全て受け入れています。パスキーの同期や Attestation がついていないことはよく「セキュリティ上問題ないか」と議論されいてますが、一般生活者向けのユビーのサービス内容を考えると、「Attestation がない」「秘密鍵が同期される」に伴い発生するセキュリティリスクより、許容する際に Email OTP 認証の課題を解決するメリットのほうが大きいと考えます。
パスキーの表示名について
パスキーを作成する際に、秘密鍵はユーザー端末のパスワードマネージャーに保存されます。ユーザーはパスワードマネージャーを利用してパスキーを管理できます。
- どのウェブサイトのパスキーが存在するかを「確認」する
- 特定のパスキーを「削除」する
といった機能は各パスワードマネージャーが基本的に提供していると思います。
それと似たような形で、パスキーの公開鍵を管理している、サービス提供者であるユビーではユーザーが作成したパスキーの一覧と、特定のパスキーを削除する機能を提供します。
パスワードマネージャーとサービス側のパスキーの表示名は、サービス側でそれぞれ決めることが可能です。この表示名は、ユーザーがパスキーを管理する際の重要な情報になりますので、適切な名前を付けるべきだと考えます。
例えば機種変更などで古い端末のパスキーを失くし、サーバー側からそれとペアとなるパスキーを削除したいとき、表示名を見てパスキーを削除しますので、わかりやすい名前をつけるとユーザーが判断しやすいです。逆に適当な名前をつけてしまうと、ユーザーが誤ってパスキーを削除してしまい、「サーバー側ではパスキーが登録されているのにログイン時に反応しなくなった」「ログイン時パスキー反応したのにログイン失敗した」[2]のような失敗が発生するリスクが高くなるので、なるべく回避したいです。
パスワードマネージャー側の表示名
パスワードマネージャー側のパスキーの表示名は、ユビー上のアカウントを識別できる名称
にするのが適切だと考えています。
ユビーには、ユーザーが自ら設定できる「アカウント名」「ニックネーム」のようなものが存在しません。その代わりに、ユーザーとの連絡手段としてメールアドレスを取得しているケースが多いです。Email OTP によるアカウント登録時に、ユーザーの検証済みメールアドレスを取得していて、ソーシャル連携の場合では外部プロバイダーからユーザーのメールアドレスをユーザーの同意を得た上で取得しています。メールアドレスはユーザーがユビーアカウントを識別するための唯一わかりやすい材料のため、パスワードマネージャー側のパスキーの表示名を基本的にメールアドレスに統一しています。
例外的なのは、LINE ログインで登録したアカウントです。ユビーは LINE からメールアドレスを取得しようとしてますが、そもそもほとんどの LINE ユーザーは LINE にメールアドレスを設定していません。メールアドレスが取れなかった場合の対策としては、LINE のニックネーム(Display Name)を取得しパスキーの表示名にしています。また、パスキー作成後にユーザーが LINE のニックネームを変更する可能性があるため、パスキー表示名にその表示名を取得した日付(=パスキーの作成日付)を併記しています。
サービス側の表示名
ユビーのアカウント設定画面上に、ログイン中アカウントの登録済みパスキー一覧を表示しています。この一覧に表示するパスキーの名前は、パスキーを登録した端末(認証器)の種類がわかる
ものが適切だと考えています。
パスキーを作成する際に、サーバーに送られるデータの中に、AAGUID というパスキーを作る認証器の一意的な識別子
を示す値があります。AAGUID と認証器の種類の関連性がわかるリストを FIDO Alliance が公開しています。また passkeydevelopers がメンテしている developer friendly な JSON ファイルも存在しており、ユビーではこの JSON ファイルを取得して利用しています。
例えばパスキーの AAGUID の値が adce0002-35bc-c60a-648b-0b25f1f05503
だったら、このパスキーは Chrome on Mac
が作成したものであることがわかるので、該当パスキーの名前を Chrome on Mac
に設定して、アカウント設定画面のパスキー一覧に表示します。
AAGUID 値が取得できなかった、もしくは AAGUID が JSON ファイルに存在いなかった可能性もありますので、そのときは HTTP リクエストの User Agent をパースして、デバイスOS名 OSバージョン ブラウザ名
のフォーマットでパスキーの表示名を構成しています。
また、それほどニーズがないとは思いますが、ユビーではパスキー表示名を変更する機能も提供しています。
パスキーの利用
ユビーのログイン画面上には、パスキーでログインする
ようなボタンを設置しておらず、Autofill UI
のみ採用しています。
ユーザーがメールアドレス認証を開始し、メールアドレス入力ページヘ遷移する際、ユビーの Javascript がブラウザにパスキー認証の要求を送信します。ユビーが作成したパスキーは Client Side Discoverable なものであるため、ユーザーはメールアドレスを入力する必要がなく、ユーザー端末に利用可能なパスキーが存在する場合、パスキー認証フローが自動的に発動されます。
パスキーが存在しない場合、UI 上何も起きないので、ユーザーは今まで通りにメールアドレスを入力して OTP で認証します。
パスキーでログイン
のようなボタンを設置するではなく Autofill UI を採用することで、端末側でパスキー登録のあり・なしによって適切な UI / UX を判断してくれる上、スムーズにフォールバックが実現できますので、「パスキー認証しようとしたのに、使えるパスキーがなくてエラーになってしまう」のような状況を改善できると考えます。
実装のはなし
パスキー(WebAuthn)の導入にあたって、それなりに実装量があったものの、W3C の仕様にデータモデルの仕様や、各オブジェクトのパース方法、もしくは登録・利用時の検証手順が詳しく記載されているため、楽しく実装できました。また CBOR のデコードなど、ひとつ下のレイヤーの処理を書くコスト・難易度が高いため、ライブラリを利用して実装するほうがよいかと考えます。
テストに関しては、サーバーにブラウザが送るデータのモックを作って、正常・異常系のユニットテストを細かく書けました。例えば、User Verification が必須と設定しているに対して、サーバーに送られた AuthenticatorData の Flag に UV が false になっている場合、サーバーがちゃんとエラーになること、くらいの粒度でテストしますので、それなりに安心感が得られます。
また e2e テスに関しては、Playwright / Chromatic を利用して、実際 navigator.credentials.create / get を呼び出し、ブラウザの virtual authenticator を反応させ、パスキーがサーバーに登録されるまでの動作を一通り確認しています。
パスキーは OS / ブラウザの実装に大きく依存する、発展中の技術で、各 OS / ブラウザのパスキー周りの動作が微妙に異なったり、ときに OS / ブラウザのバージョンアップに伴い挙動が変わるケースもあります[3]。そのため、パスキーを QA / リリースする際、各メジャーな OS / ブラウザ上で実際パスキーの動作確認を行い、動作確認済みのものを対象に opt-in 方式でパスキー機能を利用可能な端末にしています[4]。OS / ブラウザのバージョンアップがあったとき都度動作確認を行うのは大変ですが、やはり気になるので、Changelog を読むなど情報収集をしたり、パスキーに関連しそうな変更があったときに手を動かしてパスキーの動作確認しています。
終わりに
今更言うことでもないですが、パスキーはフィッシングのリスクを大きく減らすと同時に、認証体験を大きく向上する可能性のある技術だと認識しています。各プラットフォーマーのパスキーに対するサポートが拡大している中、まだ完璧とは言えないパスキーの UX に対してのネガティブな意見も出始めているようです。パスキーはまだ成熟しきった技術ではないので、Bad UX が回避できなかった際のユーザーからのネガティブな意見は理解できますが、実装者としてはこの技術が中途半端な形で終わってしまわないように願いつつ、より良いパスキー認証体験をユーザーに提供できるように努力したいと考えています。
-
OS / ブラウザの実装にもよります。異なるベンダー間の同期はまだ実現していません。また、アプリ側の実装によって、「同期しない」パスキー(Single-Device Credential)を作ることも可能です。 ↩︎
-
このような不整合を解消すべく、使えなくなったパスキーをパスワードマネージャーから自動的に削除する Signal API が存在していて Google が実装してくださいました(ユビーでは対応済み)が、現時点ではまだ Google Chrome Canary - Google Password Manager 限定になります。 ↩︎
-
記憶に残るイベントが色々ありました。
- 1 年ほど前であれば Windows の Google Chrome でパスキーを作成すると、そのパスキーは別の端末に同期されませんが、今年 9 月から同期可能なパスキーが作れるようになったようです。
- Firefox 122 が Autofill UI に対応したと言ったけど動いてなかったり、Firefox だけエラーコードがほかのブラウザと違ったりするケースもありました。
- MacOS Sequoia Beta が出た直後に、パスキーが全く動かなくなっていました(ビビりました)。
- iOS 18 の autofill ではパスキーが存在しているのにパスキー認証の動線が発動されず、パスワード入力が優先されていました。
-
現在ユビーのパスキーは、ほとんどのメジャーな OS / ブラウザをサポートしています。 ↩︎
Discussion