Androidパスキー実装の「甘い罠」 ~Jetpackの進化、WebView連携の魅力、そして assetlinks.json の落とし穴~

こんにちは。システム開発一部の浅野です。
本記事では、Androidでのパスキー実装にまつわる、一見簡単そうに見えて実は奥深い「意外な落とし穴」について共有します。

本サイトに記載されているAndroid ロボットは、Google が作成および提供している作品から複製または変更したものであり、クリエイティブ・コモンズ表示 3.0 ライセンスに記載された条件に従って使用しています。

本記事の要約

  • WebViewでパスキーを成立させるには、実装コードよりも Digital Asset Links(assetlinks.json)/ App Links の検証状態 が支配的になりやすい

  • マニフェストマージ によって「自分が意図していないドメイン」が混ざると、思わぬ形で認証が止まるケースがある

  • adb で検証状態を可視化すると、原因ドメインの特定と切り分けが一気に進む

1. はじめに

「Androidでのパスキー実装? 最新の Jetpack を使えば1行で終わりますよ」

そんな甘い言葉を信じて実装を始め、「コードは完璧なのに動かない」 という状況に遭遇したことはないでしょうか。
FIDO/パスキーの普及に伴い、Androidアプリへの実装事例も増えてきました。しかし、そこにはAndroidエコシステム特有の「OSの断片化」や「ドメイン証明の厳格さ」という、コードの外側の落とし穴が待ち受けています。

本記事では、一見魅力的な 「WebView連携」 という選択肢に潜むリスクと、エンジニアを悩ませる 「Digital Asset Links(DAL)周辺の“巻き添え”」 について、検証で見えた論点を共有します。

2. 実装パターンの比較:3つの選択肢と「悩ましい誘惑」

Androidでパスキーを実装するには、主に3つのルートがあります。それぞれのメリットと、エンジニアが直面する「葛藤」を整理します。

(A) Native実装(Credential Manager API)

【王道】 Googleが推奨する標準ルート。

  • メリット: 最高のネイティブUX。

  • デメリット: 「Webとアプリでログイン画面を二重管理する」という運用コストが重くなりがち。

(B) Chrome Custom Tabs(CCT)

【無難】 ブラウザに任せるルート。

  • メリット: 実装が楽。Chromeブラウザ本体とセッション(Cookie)を共有できるため、SSO等を利用する場合は有利。

  • デメリット: 「アプリの上にブラウザが乗っかる」画面遷移が発生する。また、認証結果をアプリに戻すためにディープリンク(App Links)の実装が必須となり、コールバック処理が複雑化する。

(C) WebView連携(Jetpack WebKit + Credential Manager)

【魅力的だが注意が必要】 アプリ内でWeb認証を完結させるルート。
以前は避けられがちでしたが、Jetpack WebKit の WebSettingsCompat.setWebAuthenticationSupport の登場により、少ないコードで「安全な委譲」を実現できる余地が増えた。

  • メリット: 既存のWeb資産を活かしつつ、アプリと完全に融合したシームレスな体験を作れる。

  • リスク: 実装が短い反面、ドメイン証明(DAL)や検証状態の管理が“ボトルネック”になりやすい

WebSettingsCompatandroidx.webkit 側のAPIです。認証情報の扱い(パスキー等)は androidx.credentials(Credential Manager)などが担い、WebView側は「WebAuthn を安全に委譲するための設定」を担います。

本記事では、あえてこの (C) WebView連携 を選んだ場合に、どこで詰まりやすいかを深掘りします。

3. 最初の論点:Legacy OS と品質要件のトレードオフ

技術選定の際、最初に立ちはだかるのが Android 9/10(Legacy OS) という壁です。

「Chromeブラウザなら Android 9 でもパスキー使えるじゃん。なんでアプリだと難しいの?」 そう思うかもしれません。しかし、アプリ開発者として仕様・実装・端末差分を踏まえると、そこにはいくつか厳しい現実があります。

「動く」と「運用できる」は違う

Chromeブラウザは、WebAuthn周辺を含めブラウザ実装として多くの面倒を抱え込み、端末差分を吸収する努力を続けています。

一方、アプリ開発者が依存するのは、主にOS標準のAPI(例:BiometricPrompt など)です。
そして Android 9/10 の時代は、実装・端末差分・強度要件の観点で “揺らぎ” が大きくなりやすい。

  • UI/UX差分: ダイアログのデザインや挙動が端末メーカーによってばらつく
  • セキュリティ強度の扱い: 強度(Strong相当)を実運用で担保するのが難しくなるケースがある

WebView連携は「OS標準機能の実力」に強く依存する

ネイティブ実装ならまだ制御の余地がある場面もありますが、WebView連携は OS標準機能(API)の実力値に大きく依存します。
ブラウザのように自前で吸収することは難しく、要件によっては対象OSを絞る判断が現実的になります。

エンジニアリングとしての判断

この「揺らぎ」を前にして、選択肢は主に2つです。

  • (1)対応範囲を広く取り、端末差分を許容しながら徹底検証する
  • (2)WebView連携の対象OSを絞り、強度と運用コストを現実的にする

特に WebView連携で、一定の認証強度と一貫したUXを重視する場合、Android 11 以降を主対象に据える判断は合理的になりやすいです。
これは手抜きではなく、パスキーのポテンシャルを引き出し、検証コストと品質を両立させるための戦略的な選択になり得ます。

4. 最大の落とし穴:リリース直前に顕在化する「見えない依存」

OSの選定も終え、Jetpackの実装も完了。開発環境での動作確認も完璧。
あとはリリース用ビルド(Release Build)を作成し、最終検証(QA)をパスすればストアに公開できる――。

しかし、その最終段階で多くのエンジニアが 「原因不明の機能不全」 に直面し、青ざめることがあります。

「開発ビルドでは動くのに、リリース候補版(RC)だけパスキーが死んでいる」

コードは1行も変えていない。変わったのはビルドタイプや署名、そして“有効になる設定”だけ。
なぜ、リリース直前の最も重要なタイミングで、アプリは沈黙するのか?

原因は「マニフェストマージ」と「静かに失敗している設定」

必死のデバッグの末に辿り着く原因は、自社のコードではなく、アプリに以前から組み込まれている サードパーティ製SDK(計測・アトリビューション等) に紐づく設定であることが少なくありません。

それも、SDKのバグというより、 これまで目立たなかった検証失敗が、パスキー導入をきっかけに“無視できない条件”として顕在化する ――という構造的な問題です。

なぜ「今さら」牙を剥くのか?

一般に、計測SDKなどはパスキー導入以前からアプリに入っており、多くの場合、問題なく稼働しています。
たとえドメイン検証(App Links / DAL)に失敗していても、SDK側がフォールバック(Scheme起動など)を持っていれば、機能が一見「動いている」ように見えてしまうことがあります。

つまり、こういう状態です。

  • 「設定に不備はあるが、目に見える実害は出ていない」
  • 「だから放置されやすい」

しかし、パスキー導入(=ドメイン証明の前提が強まる)を境に、状況が変わることがあります。

“自社ドメインだけ”見ていると見落とすポイント

パスキーは、利用する以上「ドメインに紐づく信頼(DAL)」が前提になります。
そしてアプリのマニフェストには、自分が書いた設定だけでなく、マニフェストマージによって同居した設定(=検証対象のドメイン) が含まれることがあります。

ここで厄介なのが、検証対象が「自社ドメイン」だけとは限らない点です。

  • SDKが追加するintent-filter(App Links 等)
  • SDKが利用するドメイン配下の設定(assetlinks.json 等)
  • リリースビルドでのみ有効になる挙動(設定・スイッチ)

これらが重なると、「自社ドメインは問題ないのに、別ドメインの失敗が足を引っ張る」状況が起こり得ます。

よくある assetlinks.json のつまずきポイント(チェックリスト)

ここからは一般論として、assetlinks.json 周りで詰まりやすいポイントをチェックリストとして整理します。
(自社ドメインで問題が出ない場合でも、“同居ドメイン”側で同様の罠にハマることがあります)

  • URLが正しいか
    • https://<domain>/.well-known/assetlinks.json に置かれているか(パス・拡張子含め一致)
  • HTTPSで、200が返るか
    • リダイレクト(301/302)や認証が挟まっていないか
    • 証明書エラーがないか
  • 中身が正しい JSON か
    • JSONとしてパースできるか(末尾カンマ等で壊れていないか)
  • アプリ識別子が一致しているか
    • package_name の表記揺れ(別package、別flavor)になっていないか
  • 証明書フィンガープリントが不足していないか
    • debug / release で署名が違うと、片方だけ登録して「片方だけ動く」になりやすい
    • “本番想定の署名”が漏れていないか(CI/CDや配布経路で署名が変わるケースを含む)
  • キャッシュの罠
    • CDNやキャッシュで古いassetlinks.jsonが配信されていないか(反映待ちに見えることがある)
  • 「自分で管理できないドメイン」の罠
    • SDKが提供するドメイン配下の assetlinks.json は、自分のサーバで直接編集できないことがある
      → その場合、SDK側の管理画面等で署名情報を登録し、SDK側が配信する assetlinks.json に反映させる必要が出ることがある

「パスキーは自社ドメインだけ整えればOK」と思っていると、この最後の項目で不意打ちを食らいがちです。

検証状態の可視化:adb で「どのドメインが落ちているか」を見る

「結局、どのドメインが原因なのか?」を最短で切るには、OSが見ている検証状態をそのまま見るのが強力です。
(端末/OSバージョンにより出力は変わりますが、概ね以下の方針が有効です)

# 例:App Links の検証状態を確認(出力にドメインごとの状態が出る)
adb shell pm get-app-links <package_name>

# 例:検証の再実行(設定変更後に反映確認したいとき)
adb shell pm verify-app-links --re-verify <package_name>

ここで「失敗しているドメイン」が見つかれば、次のアクションが具体化します。

  • そのドメインが 自分の設定なのか / SDKが追加したものなのか を棚卸しする
  • SDK由来なら、SDKの管理画面や設定手順で “署名情報の登録漏れ” がないか を確認する
  • そもそも不要なドメインなら、マニフェスト側で autoVerify の扱いを見直す(可能なら削る/無効化する)

「原因がコードではない」問題ほど、こうした“見える化”が効きます。

5. 誤解と真実:ネイティブ実装なら回避できたのか?

ここで、「WebViewなんか使うからだ。おとなしくNative実装しておけばよかったのに」という声が聞こえてきそうです。

ただ、多くのケースで言えるのは、実装方式を変えても“ドメイン証明(DAL)”そのものは避けられないという点です。

Credential Manager を使おうが WebView を使おうが、パスキーでドメインに紐づく信頼を扱う以上、DALによる証明は前提になります。
したがって、ドメイン証明に関連する設定不備や、マニフェストマージによる想定外のドメイン混入は、方式を変えただけで消えるとは限りません。

「どの方式でも、コードの外側を先に疑う」
これが、パスキー導入時の現実的なスタンスになります。

6. コラム:その時、iOSはどうだったか

一方、iOSエンジニアはこの手の「巻き添え事故」とは比較的距離を置けることがあります。
iOSにも同様のドメイン証明ファイル(AASA: apple-app-site-association)が存在しますが、Androidのような形で影響が波及しにくい場面があるのは事実です。

理由の一つは、Entitlements(権限)の分離が明確であることです。

iOSの Associated Domains 設定では、パスキー用の設定(webcredentials:)と、ディープリンク用の設定(applinks:)が、それぞれ独立したサービスとして定義されます。
OSはそれらを個別に評価するため、「ディープリンク設定が死んでいる」からといって「パスキー側の権限まで連鎖的に扱う」状況が起こりにくい、という見方ができます。

7. まとめ:コードを書かない場所こそ疑え

Jetpackの進化により、Androidでのパスキー実装コードは劇的に短くなりました。
しかし、「コードが短い」ことは「簡単である」ことを意味しません。

むしろ、コード量が減った分、比重は 「インフラ設定(assetlinks.json)」「マニフェストマージによる見えない依存」 に移っています。

「犯人はコードの中にいない」

パスキー実装で沼にハマったときは、この言葉を思い出してください。
疑うべきは自社ドメインの設定だけではありません。アプリの片隅で静かに動いている 「思いもよらぬ依存」 が、真因になっているかもしれません。