Android WebView本人認証エラーの改善

Android WebView本人認証エラーの改善

1. 作業の背景

今回の作業は、本人認証機能をゼロから設計した事例ではなく、既存の実装で発生したエラーを分析・修正した保守事例です。プロジェクトでは、React Nativeアプリの共通WebViewからVueで実装されたWeb画面を提供しており、その画面にはN*** CheckPlus本人認証がすでに連携されていました。

WebブラウザとiOSアプリでは正常に動作していましたが、React Native Android WebViewでのみ認証に失敗していました。担当範囲は既存の連携構造を全面的に変更することではなく、プラットフォームごとの差異を基準に原因を絞り込み、共通WebViewの他の機能への影響を最小限に抑えながら認証フローを正常化することでした。

2. エラーの発生と分析の手がかり

2.1 確認されたエラー

AndroidアプリのWebViewでN***本人認証を進めると、認証が完了せず、失敗ページへ遷移しました。当時の記録には次のエラーが残っていました。

SecurityError: Failed to read a named property 'checkSuccess' from 'Window':
Blocked a frame with origin "https://n***.checkplus.co.kr"
from accessing a cross-origin frame.

このメッセージは、異なる出所(Origin)のWindowまたはFrame間で、相手側のウィンドウのプロパティに直接アクセスしようとしたため、同一オリジンポリシー(Same-Origin Policy)によってブロックされたことを意味します。ただし、checkSuccessは自プロジェクトで作成した関数ではなく、N***認証画面内部で使用されていた名前と思われたため、自作のVueコードでこの関数を直接修正するというアプローチは取れませんでした。

また、画面に最後に表示された例外が最初の失敗原因だと断定もしませんでした。認証フローが前の段階ですでに失敗した後、失敗ページの後続スクリプトが実行される過程で、クロスオリジン(Cross-Origin)例外が発生した可能性があったためです。

2.2 実行環境とURL遷移フローの比較

同じ機能を実行環境ごとに比較した結果、Webブラウザ、同じAndroid端末の一般的なブラウザ、iOSアプリでは正常に進行し、React Native Android WebViewでのみ失敗していました。この違いから、N***サービス全体やVueのビジネスロジックよりも、Android WebViewのURL処理フローを優先して確認しました。

失敗直前の記録では、次のURL遷移フローを確認しました。

/cert/mobileCert/main
> /cert/mobileCert/method
> /cert/mobileCert/fail/applink
> SecurityError 발생

SecurityErrorより前に/fail/applinkのパスを経由していた点が重要な手がかりでした。この記録だけでN***内部の正確な失敗原因を確定することはできませんが、最後のCross-Origin例外より前にある外部認証アプリの呼び出し段階が先に失敗していたかを確認する必要がありました。

3. 既存コードの確認と原因範囲の絞り込み

3.1 実際の修正箇所の確認

Androidでのみ発生した問題だったため、当初はJavaまたはKotlinのWebViewClientを修正する必要があるか検討しました。しかし、プロジェクトのAndroid ActivityはWebViewを直接生成する構造ではなく、実際の画面はreact-native-webviewをラップしたTypeScript共通コンポーネントで構成されていました。

したがって、修正箇所はAndroid Activityではなく、React Native共通WebViewコンポーネントでした。Androidで発生したエラーであっても、そのプロジェクトがネイティブ機能をどの層で制御しているのかを最初に確認しなければ、不要な修正範囲を広げることになります。

3.2 既存設定と自作コールバックフローの分離

既存の共通WebViewには、originWhitelist={['*']}、JavaScript、DOM Storage、マルチウィンドウ、およびプラットフォームごとのCookie関連設定がすでに適用されていました。AndroidではthirdPartyCookiesEnabled, domStorageEnabledsetSupportMultipleWindowsなどが存在していたため、単純なCookieまたはストレージオプションの不足を第一の原因と考えるのは難しい状況でした。

originWhitelistは、WebViewが許可するナビゲーション対象の範囲を指定するプロパティであり、同一オリジンポリシーを無効にしたり、外部アプリの起動を保証したりする設定ではありません。そのため、設定値だけで原因を断定せず、当時確認した共通コンポーネントに外部アプリURLを明示的に分岐するハンドラーが存在しなかったという事実を基準に、原因の範囲を絞り込みました。

プロジェクトには、PCのポップアップ結果ページがwindow.opener.callbackEncodeData()を呼び出す別のフローもありました。一方、モバイルアプリではフォームを_selfに送信した後、戻ってきたページでEncodeDataを処理する構造であり、今回のエラーに表示された関数名はcheckSuccessでした。そのため、PCポップアップのコールバック問題とAndroid WebViewの外部アプリ呼び出し問題を、同じ原因として混同しませんでした。

マルチウィンドウ設定の変更も候補として検討しましたが、実際の解決策には適用しませんでした。正常な動作を確認できた措置のみを解決内容に含めました。

3.3 外部アプリのカスタムスキーム処理の確認

N***本人認証の過程でP***などの外部認証アプリを起動する際は、一般的なhttp://またはhttps://のアドレスではなくintent: URIや tauthlink: のようなカスタムスキームを使用できます。当時の作業メモにも、機密性の高いパラメータを除いた tauthlink://sktauth?... 形式が残っていました。

一般的なモバイルブラウザでは、このようなURLをOSのアプリ起動フローに渡せますが、アプリ内のWebViewでは、URL遷移リクエストを横取りしてReact Nativeまたはネイティブ層に渡さなければならない場合があります。

当時確認した共通WebViewコンポーネントには、Cookieやウィンドウ関連の設定はありましたが、HTTPではない認証アプリURLを明示的に区別し、外部アプリに渡すハンドラーはありませんでした。プラットフォーム別の再現結果、 /fail/applink の遷移履歴、既存コードの不足箇所を総合し、まずAndroidの外部認証アプリURL処理から補完しました。

4. 解決方法の適用

4.1 処理基準

onShouldStartLoadWithRequestを使用して認証中に発生するURL遷移リクエストを確認し、WebViewで引き続き処理するリクエストと、OSに渡す外部アプリリクエストを分離しました。今回の問題のURLは初回ロードではなく認証過程で発生した遷移リクエストだったため、この処理方法で対応できました。

処理基準は次のように整理しました。

  • http://, https://, about:blankはWebViewで引き続き処理します。

  • Androidで確認した認証アプリURL(intent:, tauthlink:)のみ、外部アプリ起動の対象として許可します。

  • 外部アプリに渡したURLは、WebViewが重複してロードしないように falseを返します。

  • 許可リストにないスキームは実行せず、WebViewのロードも中止します。

  • iOSでは、このハンドラー内で別途Linkingの呼び出しは行いません。

4.2 主要コード

以下のコードは、当時適用した主要な処理構造を理解しやすいように簡略化した例です。

const handleShouldStartLoadWithRequest = (
  {url}: {url: string},
): boolean => {
  if (/^(https?:\/\/|about:blank(?:#.*)?$)/i.test(url)) return true;
  if (Platform.OS !== 'android') return true;

  const isIntentUrl = /^intent:/i.test(url);
  const isTAuthUrl = /^tauthlink:/i.test(url);
  if (!isIntentUrl && !isTAuthUrl) return false;

  const targetUrl = isIntentUrl ? parseIntentUrl(url) : url;
  if (!targetUrl) {
    showAuthAppError();
    return false;
  }

  void Linking.openURL(targetUrl).catch(showAuthAppError);
  return false;
};

要点は、通常のWeb URLと許可されたAndroid認証アプリURLを分離し、Linking.openURL()で外部アプリの起動を要求した後、WebViewでの該当遷移を中止することです。

parseIntentUrl()は、当時確認した intent: 形式から実際のアプリスキームURLを構成する補助関数です。当時のメモでは、アプリの起動に失敗した場合に getFallbackUrl()S.browser_fallback_urlを確認する別の分岐も設けていました。上の例にはURL遷移を分離する主要な構造だけを残しており、両関数はすべてのAndroid Intent URIを解析する汎用パーサーではなく、実際のサービスで確認した形式に合わせて使用範囲を制限する必要があります。

純粋なカスタムスキームである tauthlink:には、アプリのパッケージや代替URLの情報がない場合があるため、実行に失敗した際に案内だけを表示するのか、スキームとパッケージのマッピングを管理してストアに移動させるのかは、別途ポリシーとして決定する必要があります。

共通コンポーネントが ...restで外部WebViewプロパティを渡す構造になっている場合は、既存のonShouldStartLoadWithRequestがあるか確認する必要があります。単純にプロパティの順序によって一方を上書きするよりも、既存のハンドラーと共通ハンドラーの戻り値を合成する方式が安全です。

5. 再テスト結果

当時の修正後、以前に失敗していたAndroid環境でN***本人認証を再度実施しました。通常のhttps:// 認証ページはWebViewで引き続き開かれ、認証アプリのカスタムスキームはWebViewが直接ロードせず、外部アプリの起動リクエストとして渡されました。その結果、P***などの外部認証アプリの呼び出しとN***本人認証の手順が続行され、従来の/fail/applink および SecurityErrorが続く失敗フローは、同じシナリオでは再現されなくなりました。

この結果は、カスタムスキームの処理ロジックが同一生成元ポリシーを変更したことを意味するものではありません。外部アプリの呼び出しが正常な経路へ進むことで、/fail/applink の後続スクリプトが実行される失敗経路に入らなくなり、その結果、その後に発生していた Cross-Origin 例外も現れなくなったと解釈しました。

ただし、カスタムスキームの処理漏れが SecurityErrorの唯一の内部原因だったと断定したわけではありません。N*** 内部で checkSuccessが呼び出される全体構造を直接確認したわけではないためです。当時の記録から確認できる事実は次のとおりです。

  • Android アプリの WebView でのみ認証に失敗しました。

  • 失敗時には /fail/applink の経路を経た後に SecurityErrorが発生しました。

  • 当時確認した共通 WebView には、外部認証アプリの URL を明示的に処理するロジックがありませんでした。

  • その処理を追加した後、同じ認証シナリオが正常化しました。

したがって今回の事例は、「Cross-Origin ポリシーを回避してエラーを除去した」のではなく、「Android WebView の外部認証アプリ URL 処理ロジックを補完した後、先行する失敗経路と後続の Cross-Origin 例外が同じシナリオでは再現されなくなった」と整理するのが正確です。

6. 適用時の考慮事項

共通 WebView に URL 遷移ハンドラーを追加すると、N*** 以外の画面も同じロジックを通る可能性があります。HTTP 以外のすべての URL を外部アプリに渡すと、意図しないアプリの起動や既存機能の変更が発生する可能性があるため、認証画面または認証処理中に適用範囲を限定し、確認済みのスキームのみを許可する方法が安全です。

intent: URI を汎用的にサポートする必要がある場合は、内部で実際に使用されるスキーム、パッケージ、代替 URL のドメインも許可リストで検証する必要があります。認証 URL 全体をログに残すと識別子やトークンが含まれる可能性があるため、スキームやホストなど必要な情報だけを記録し、機密性の高いパラメーターはマスキングする必要があります。

7. 作業過程で得られた知見

7.1 最後のエラーより前のフローを確認する必要がある

最初に確認したメッセージは Cross-Origin SecurityErrorでしたが、URL 遷移の記録では、その前に /fail/applink の経路へ移動していました。最後の例外だけを直接修正しようとしていたら、外部アプリの呼び出しが先に失敗していた兆候を見落としていた可能性があります。障害分析では、例外に到達する前の URL 遷移と状態の変化も併せて確認する必要があります。

7.2 プラットフォーム比較によって分析範囲を絞り込める

Web ブラウザーと iOS アプリでは正常で、React Native Android WebView でのみ失敗するという事実から、分析対象を迅速に絞り込むことができました。同じ Android 端末の一般ブラウザーとアプリ WebView を比較したことも、OS 自体の違いと WebView の統合方式の違いを区別するうえで役立ちました。

7.3 適用した対策と検討した代替案を区別する必要がある

分析過程では、ウィンドウの処理方式やマルチウィンドウ設定など、複数の候補を検討しました。しかし、正常動作を確認できた核心的な変更は、onShouldStartLoadWithRequestで許可された外部アプリ URL を判別し、Linkingに渡す処理でした。技術事例を整理する際は、実際に適用・検証した対策と、可能性として検討した代替案を区別することで、結果を誇張せずに済みます。

8. まとめ

今回の作業は、既存の React Native ハイブリッドアプリで Android WebView にのみ発生していた N*** 本人認証エラーを分析し、改善した事例です。Cross-Origin エラーから調査を始めましたが、プラットフォームごとの再現結果、失敗直前の URL 遷移フロー、既存の共通 WebView 設定と独自コールバック構造を順に確認することで、外部認証アプリ URL の処理へと分析範囲を絞り込みました。

当時の記録を基に、一般的な Web URL と Android 認証アプリ URL を分離し、許可されたスキームを React Native の Linkingに渡すよう補完しました。修正後は、同じ失敗シナリオにおいて外部認証アプリの呼び出しと N*** 本人認証が正常に継続し、/fail/applink および後続の SecurityErrorは再現されなくなりました。

この事例を通じて、画面に最後に表示されたエラーだけを直接修正するのではなく、エラー以前の URL 遷移と、Web とネイティブの境界で実際にどのような処理が欠落していたのかを確認することが重要だと学びました。また、確認できた事実と推論を区別し、技術事例の結論を誇張しないことも重要でした。

Site footer