🚀

WebView×OAuth2実践ガイド🚀—Flutter + AppAuthで安全ログインを作る

に公開
5

🚀 はじめに

モバイルアプリに OAuth 2.0 ログインを組み込むとき、「とりあえず WebView でログイン画面を開けばいいでしょ?」と考えがち。しかし実際には Cookie 共有・生体認証・Deep Link…課題が山盛りです。本稿では ネイティブ+ WebView アプリを題材に、AppAuth(Flutter)で安全&爆速に OAuth 2.0 を実装する手順を紹介します。

📚 WebView × OAuth2 の落とし穴

  • WebView ログインは基本 NG ― RFC 8252 で非推奨
  • Cookie ストアが分離 ― WKWebView は Safari と別
  • リダイレクト受信が複雑 ― JS Bridge やポーリングが必要

解決策は 「認可コードはネイティブで、WebView にはセッションだけ渡す」 型!

🏗 アーキテクチャ

ASWebAuthenticationSession/Chrome Custom Tabs を使うだけでキーチェーン+ Face ID 自動連携 👌

🔧 Flutter 実装

1️⃣ 依存

dependencies:
  flutter_appauth: ^6.0.2
  uni_links: ^0.5.1
  webview_flutter: ^4.7.0
  webview_cookie_manager: ^2.0.1

2️⃣ 認可コード取得

dart
コピーする編集する
final auth = FlutterAppAuth();
final res = await auth.authorize(
  AuthorizationRequest(
    'DEMO_CLIENT', 'com.example.app:/oauth',
    serviceConfiguration: AuthorizationServiceConfiguration(
      authorizationEndpoint: 'https://idp.example.com/oauth/authorize',
      tokenEndpoint: 'https://idp.example.com/oauth/token',
    ),
    scopes: ['openid', 'profile'],
    preferEphemeralSession: true,
  ),
);
final code = res.authorizationCode; // 認可コードを取得

3️⃣ BE でセッション発行

dart
コピーする編集する
final r = await dio.post('/api/auth/oauth',
    data: {'code': code}); // 認可コードをバックエンドに送信
final sessionId = r.data['session_id'];

dart
コピーする編集する
await WebviewCookieManager().setCookies([
  Cookie('session_id', sessionId)
    ..domain = 'app.example.com'
    ..path = '/'
    ..secure = true
    ..httpOnly = true,
]);
Navigator.push(context, MaterialPageRoute(
  builder: (_) => const WebView(
        initialUrl: 'https://app.example.com/',
      ),
));

iOS (Universal Link)

  1. Xcode で Associated Domains を有効
  2. applinks:your.domain.com を追加
  3. .well-known/apple-app-site-association を配置(/oauth* を許可)

Android (App Link)

AndroidManifest.xmlintent-filter を宣言し、

assetlinks.json をサーバに置いて検証ツールで確認。

💡 ポイント

  • 可能なら https の Universal/App Link を採用し、カスタムスキームは最小限に
  • Flutter では uni_links で iOS/Android 共通コードで受信できる

⚔️ AppAuth と手作り実装の比較

AppAuth 手作り (HTTP + WebView)
認可 URL 生成 ✅ 自動 ⚠️ 手動で組立
PKCE 対応 ✅ 標準 ❌ 自前実装
AuthSession / CustomTabs ✅ ラップ済 ⚠️ プラグイン要
保守コスト 🔽 低 🔼 高

🚀 ステージング用 IDP で先行開発するメリット

  • チームの手が止まらない:ID 基盤完成を待たずクライアント側を完了
  • 差し替え一瞬:エンドポイントとクライアント ID を .env で切替
  • QA 前倒し:再認可・refresh 失効などのエッジケースを早期検証

規格に乗っかる = 最大の生産性ブースト 💪

🩹 よくあるハマり

症状 原因
ログインループ Cookie ドメイン or SameSite=None 抜け
iOS だけ未ログイン Cookie 注入 →WebView 起動順が逆
Face ID 出ない WKWebView で開いている

🎉 まとめ

  • AppAuth なら ID プロバイダ非依存で認可コード取得
  • コードはネイティブで受取り、WebView には Cookie だけ 渡すのが安全
  • 生体認証・自動入力は OS が面倒を見る
  • 仮 IDP で PoC → 本番基盤へ差し替えれば工数ゼロ

この型を覚えれば次の案件でもコピペで爆速実装できますね 🚀

GitHubで編集を提案

Discussion

DiegoDiego

記事、大変参考になりました!ありがとうございます。
一点だけ気になったのですが、記事内で「codeをBEに渡している」といった記述がいくつか見られる箇所について、実際には認証サーバーとのやり取りで得た「アクセストークン」をバックエンドに渡してセッションを確立する流れかと理解しました。
もし「認可コード」ではなく「アクセストークン」を指しているのであれば、そのように明記されると、OAuthのフローに馴染みのない読者にとってはより誤解なく理解が進むかと思いました。

もももも

ご指摘ありがとうございます!おっしゃる通りでした。

標準的なOAuth2.0フローでは「認可コード」をバックエンドに渡すのが一般的です。シーケンス図では正しく描かれていましたが、サンプルコードでは「アクセストークン」を渡す実装になっていました。

この不一致を修正し、サンプルコードを「認可コード」をバックエンドに渡す形に更新しました。貴重なフィードバックに感謝します!

DiegoDiego

ありがとうございます!
若干私の解釈と違いがあり、質問させてください。
多分認可コードをBEに渡すのは、推奨されていないと思っています。
認可コードはクライアントでトークンを取得するために使用するべきかなと思います。。

なので、authorizeAndExchangeCodeメソッドを使用して、アクセストークンまで取得し、アクセストークンを下にセッション生成が良いかなと思いました!

以下、Geminiに聞いてみた結果です。

OIDC(OpenID Connect)において、フロントエンド(ユーザーエージェント、通常はブラウザ)で取得した認可コードをリソースサーバーに直接渡して、リソースサーバーがその認可コードを使ってIDプロバイダー(IdP)からトークン(アクセストークン、IDトークンなど)を取得する構成は、一般的には推奨されませんし、セキュリティ上の懸念があります。
以下にその理由と、より一般的な構成について説明します。
推奨されない理由(セキュリティ上の懸念点):

  • 認可コードの不正利用リスク: 認可コードは一度しか使用できない使い捨てのコードですが、リソースサーバーに渡すまでの経路や、リソースサーバー自体が侵害された場合、悪意のある第三者に認可コードが漏洩し、不正にトークンを取得される可能性があります。
  • クライアントの秘匿性の問題: 通常、認可コードとトークンを交換する際には、クライアントIDとクライアントシークレットが必要になります(Public Clientの場合はPKCEを利用)。リソースサーバーがこの処理を行う場合、リソースサーバーがクライアントの資格情報(特にクライアントシークレット)を保持する必要が出てきます。これは、リソースサーバーの役割を不必要に拡大させ、セキュリティリスクを増大させます。
  • 責任範囲の不明確化: OIDCのフローでは、クライアント(通常はバックエンドアプリケーション)が認可コードをIdPに送信し、トークンを取得する責任を負います。この責任をリソースサーバーに移すと、アーキテクチャが複雑になり、責任範囲が不明確になる可能性があります。
  • 意図しないトークンの流通: リソースサーバーが取得したトークンをどのように扱うかにもよりますが、意図しない形でトークンがフロントエンドに渡ったり、他のコンポーネントに共有されたりするリスクが生じます。
    一般的なOIDC認可コードフロー:
    より一般的で推奨されるOIDCの認可コードフローは以下のようになります。
  • 認可リクエスト: フロントエンド(ブラウザ)がユーザーをIdPにリダイレクトし、認可を要求します。
  • 認可コード取得: ユーザーがIdPで認証・認可を行うと、IdPは認可コードを発行し、フロントエンド(アプリ)経由でクライアントアプリケーション(通常はバックエンドサーバー)の指定されたリダイレクトURIに送り返します。
  • トークンリクエスト: クライアントアプリケーション(バックエンドサーバー)は、受け取った認可コードと、自身のクライアントID、クライアントシークレット(またはPKCEのコードベリファイア)をIdPのトークンエンドポイントに送信し、アクセストークンやIDトークンを要求します。
  • トークン取得・検証: IdPは認可コードを検証し、問題がなければアクセストークンやIDトークンを発行します。クライアントアプリケーションはこれらのトークンを受け取り、IDトークンを検証します。
  • リソースアクセス: クライアントアプリケーション(またはフロントエンドがクライアントアプリケーション経由で)は、取得したアクセストークンを使ってリソースサーバーに保護されたリソースを要求します。
  • リソースサーバーの検証: リソースサーバーは、受け取ったアクセストークンを検証し(通常はIdPの公開鍵やイントロスペクションエンドポイントを利用)、正当なものであればリソースへのアクセスを許可します。
    リソースサーバーの役割:
    リソースサーバーの主な役割は、アクセストークンを検証し、保護されたリソースへのアクセスを制御することです。トークンの取得プロセスに直接関与することは通常ありません。
    代替案(BFF: Backend For Frontend パターンなど):
    もしフロントエンドとリソースサーバーの間に何らかのバックエンドコンポーネントを置くことができるのであれば、BFF(Backend For Frontend)パターンを検討できます。この場合、BFFがOIDCクライアントとしての役割を担い、フロントエンドから認可コードを受け取ってトークンを取得し、適切に管理・利用することができます。
    まとめ:
    フロントエンドで取得した認可コードを直接リソースサーバーに渡してトークンを取得する構成は、セキュリティリスクや責任範囲の観点から推奨されません。OIDCの標準的なフローに従い、クライアントアプリケーション(通常はバックエンド)が認可コードを扱ってトークンを取得し、そのアクセストークンをリソースサーバーでのリソースアクセスに利用する形が望ましいです。
ritouritou

多分認可コードをBEに渡すのは、推奨されていないと思っています。
認可コードはクライアントでトークンを取得するために使用するべきかなと思います。。

なので、authorizeAndExchangeCodeメソッドを使用して、アクセストークンまで取得し、アクセストークンを下にセッション生成が良いかなと思いました!

バックエンド+フロントエンドで構成されるOAuth Clientでは、認可コードをバックエンドに渡すことは適切です。

このようなケースでは、アクセストークンをバックエンドに渡すことがアンチパターンです。
アクセストークンはリソースサーバーが検証して利用するものであり、クライアントは本来その中身を知らなくても良いものであり、クライアントによるアクセストークン検証はOAuthの仕様で定義されていません。

  • フロントエンド単体でOAuthクライアントとして動作
  • フロントエンド+バックエンドでOAuthクライアントとして動作

この2つでは認可フローも当然異なります。この辺りを解説した記事を書いているので参考にしてみてください。

https://zenn.dev/ritou/articles/d26c7861047a2d

フロントエンドで取得した認可コードを直接リソースサーバーに渡してトークンを取得する構成は、セキュリティリスクや責任範囲の観点から推奨されません。

このGeminiの回答は、「リソースサーバーに認可コードを投げるのはNG」みたいな解説なのでそれは質問にあるバックエンドに認可コードを投げるのは適切かとは別の話です。認可コードを受け付けて良いという部分の説明としては、以下の部分です。

フロントエンドとリソースサーバーの間に何らかのバックエンドコンポーネントを置くことができるのであれば、BFF(Backend For Frontend)パターンを検討できます。この場合、BFFがOIDCクライアントとしての役割を担い、フロントエンドから認可コードを受け取ってトークンを取得し、適切に管理・利用することができます。

そしてそもそも、リソースアクセスのための仕組みであるOAuthをログインの目的で利用することが厳密には正しくない(OAuth認証()と呼ばれているもの)ことも認識しておきましょう。
https://ritou.hatenablog.com/entry/2020/12/01/000000

DiegoDiego

なるほど!
私が勘違いしてました!!
丁寧なご説明ありがとうございます!理解しました!!