2026.09.30
OIDC(OpenID Connect)を使ったソーシャルログインは、仕組みだけを見ると単純に思えます。しかしネイティブアプリには「クライアントシークレットを安全に保持できない」「認可コードを外部アプリや悪意あるスキームハンドラに横取りされ得る」といったWebブラウザとは異なる制約があり、そこから生まれる脅威への対策が必要です。
本稿では、なるべく少ないパラメータでモバイルアプリでGoogle認証・Apple認証を実装する際に、PKCEとnonceを中心にどのような脅威を想定し、どう対策するかを一般的な設計知識として整理します。
OIDCフローの選定
OIDCには3つのフローがありますが、ネイティブアプリでは認可コードフロー(+ PKCE)以外を選ぶ理由は基本的にありません。
| フロー | トークンの受取方法 | 主なリスク | ネイティブアプリでの適性 |
|---|---|---|---|
| 認可コードフロー | バックチャネル(トークンエンドポイントへの直接通信)で取得 | 残る主な脅威はPKCEで対策できる | ○(推奨) |
| インプリシットフロー | フロントチャネル(リダイレクトURL)でトークンを直接受け取る | トークンがURL経由で露出しやすく、置換攻撃への根本対策ができない | × |
| ハイブリッドフロー | 認可コードと一部トークンをフロントチャネルで同時取得 | インプリシットのリスクを引き継ぎ、メリットは乏しい | × |
ネイティブアプリはOAuthの分類上「パブリッククライアント」(クライアントシークレットを安全に保持できない)に当たります。PKCEを併用することで、クライアントシークレットなしでも認可コードフローを安全に利用できるのが、現在のOAuth 2.0 for Native Apps(RFC 8252)の考え方です。
脅威モデルと対策
なるべく少ないパラメータで対策したい場合、中心になるのはPKCEとnonceです。
| 脅威 | 内容 | 対策 |
|---|---|---|
| CSRF | 攻撃者が仕掛けた認可リクエストにユーザを乗せる | state、またはnonce+PKCE(stateの方が検知が早い) |
| 認可コード横取り攻撃 | リダイレクト途中で認可コードを第三者に取得される | PKCE |
| コードインジェクション | 攻撃者自身の認可コードを被害者のセッションに注入する | PKCE、nonce、c_hash |
| リプレイ攻撃 | 過去のIDトークン・認可レスポンスを再利用する | nonce |
| トークンインジェクション | response_typeにtokenを含むフローで、攻撃者が入手した別のアクセストークンを正規クライアントに注入する。入手経路の例:同じカスタムスキームを登録した他アプリにアクセストークンを横取りされる | at_hash(本設計は認可コードフローのみを使うため、この脅威自体が発生しない) |
CSRF対策としてstateを使わずにnonce+PKCEで代用する選択は、OAuth 2.0 Security Best Current Practice(RFC 9700)の考え方とも整合します。攻撃者が自分の認可コードを被害者側に注入しても、そのコードは攻撃者のcode_challengeに紐づいているため、被害者のcode_verifierでは交換に失敗します。ただしstateを使わない場合、不正な認可レスポンスの検知がトークン交換やIDトークン検証の段階(=フローの後半)になる点は意識しておく必要があります。
PKCEの仕組み
CSRF、認可コード横取り攻撃、コードインジェクションの対策としてPKCEを用います。
PKCEダウングレード攻撃を防ぐため、code_challenge_methodにはplainではなく必ずS256を指定します。code_verifierは43~128文字の文字列で、32バイトの乱数をBase64URLエンコードして生成するのが一般的です(43文字になります)。
nonceの仕組みと注意点
リプレイ攻撃対策にnonceを使用します。生の値をそのまま送らず、SHA256でハッシュ化してからプロバイダに送信する設計もよく用いられます(URLに生のnonceが残ることを避けるため)。
検証時の比較対象:GoogleもAppleも、受け取ったnonceパラメータを再ハッシュ化せずそのままIDトークンのnonceクレームに埋め込みます。つまりバックエンドが検証すべきなのは「元のnonceをハッシュ化した値」と「IDトークンのnonceクレーム」の一致です。
もう一つ見落とされがちな点として、バックエンドが検証に使う元のnonceを、そもそもネイティブアプリから受け取っているかを確認する必要があります。IDトークンだけでは検証対象の値が欠けてしまいます。
全体フロー:Google認証
Google認証は、ネイティブアプリ自身が認可コードをIDトークンに交換し、バックエンドにはIDトークンのみを送る構成です。
バックエンドはIDトークンを受け取るだけで、自らトークン交換に関与しません。その分、nonceの検証が唯一の「このIDトークンは自分が開始したログインの結果か」を確認する手段になるので、前節の通り実装を正確に行うことが重要です。
全体フロー:Apple認証(iOS)
iOSではAppleのSign in with Apple SDKが直接IDトークンを返すため、本構成では認可コードの交換もPKCEも使いません(SDKは認可コードも返しますが、利用しません)。ブラウザリダイレクトを介さないので認可コード横取りのリスク自体がなく、PKCEは不要です。
nonceの扱いはGoogleと同じです:ハッシュ値をSDKにnonceとして渡し、IDトークンのnonceクレームにはそのハッシュ値がそのまま入ります。
全体フロー:Apple認証(Android)
AndroidにはSign in with AppleのネイティブSDKがないため、Custom TabsなどのブラウザでWebの認可フローを使います。Appleのredirect_uriはhttpsのURLに限られ、name/emailスコープを要求するとresponse_mode=form_postが必須になるため、認可レスポンスはまずバックエンドのコールバックがPOSTで受け取ります。コールバックはそのままトークン交換とIDトークン検証を行い、アプリにはApp Linksでワンタイムコードだけを返します。nonceはログイン開始時にバックエンドが生成・保存し、IDトークンのnonceクレームから該当するログインセッションを特定します。アプリはワンタイムコードとログインセッションIDを送って自社JWTを受け取るため、認可コードはアプリもURLも経由しません。App Linksのため、リダイレクト先のドメインに/.well-known/assetlinks.jsonを配置してアプリと紐づけます。Appleの公式ドキュメントにPKCEの記載はないため、この構成ではclient_secretとnonceで守ります。stateは省略しても、nonceとログインセッションIDの突き合わせで不正な応答を検知できます(検知はトークン交換の後になります)。
Appleのトークンエンドポイントではclient_secretが必須です。client_secretは有効期限付きのJWTで、Apple Developerポータルで取得した.p8秘密鍵を使ってES256で署名して生成します。
App Linksの検証が失敗した場合(未インストール等)、フォールバックとしてブラウザ経由で実URLへのリクエストが発生し、CDN/サーバーのアクセスログにワンタイムコードを含むクエリ文字列が残る可能性があります。ログのクエリ文字列マスキングや、可能であればログ保持期間を短縮します。
まとめ
ネイティブアプリでのOIDC実装は、以下を押さえておく必要があります。
- ・フローは認可コードフロー + PKCE(S256)を選び、インプリシット/ハイブリッドは避ける
- ・stateを使わない場合は、nonce+PKCEでCSRF対策を代用できることを確認する
- ・nonceをハッシュ化して送信する場合、バックエンドの検証もハッシュ値同士を比較する(元のnonceとの直接比較は常に失敗する)
- ・バックエンドはIDトークンの署名・iss・aud・nonce・exp・email_verifiedをすべて検証し、想定外の署名アルゴリズム(alg混同)を拒否する
- ・fullNameなど、IDトークンの署名済みクレームに含まれない付帯データは未検証の値として扱う
- ・AndroidでApp Linksを使う場合、フォールバック時のリダイレクトURLがアクセスログに残る可能性を考慮する
どれも目立たない項目ですが、1つでも抜けると「正規のログインまで失敗する」か「エラーは出ないのに攻撃を防げていない」かのどちらかになりがちです。実装やレビューの際に参考にしてください。



