ネイティブモバイルアプリケーション向けPKCE

カスタムURLスキームとユニバーサルリンクを使用してiOSとAndroidネイティブアプリケーションでOAuth 2.0認可コード + PKCEを実装する方法。

PKCE

詳細な説明

モバイルアプリ向けPKCE

モバイルアプリケーションはパブリッククライアントです。アプリバイナリは逆コンパイル可能なため、client_secretを安全に保存できません。PKCEは、iOS、Android、その他のネイティブプラットフォームで認可コードフローを保護するために不可欠です。

モバイル固有の考慮事項

カスタムURLスキーム vs ユニバーサルリンク

モバイルアプリは認可コールバックを受信するためにリダイレクトURIを登録します:

方法 セキュリティ
カスタムURLスキーム myapp://callback 低 — 任意のアプリが同じスキームを登録可能
ユニバーサルリンク(iOS) https://app.example.com/callback 高 — ドメイン所有権の検証
App Links(Android) https://app.example.com/callback 高 — ドメイン所有権の検証

カスタムURLスキームよりもユニバーサル/App Linksを常に優先してください。カスタムスキームは同じスキームを登録する悪意のあるアプリによる傍受に脆弱です。

モバイルのフロー手順

  1. フロー開始前にアプリ内でPKCEペアを生成
  2. システムブラウザを開く(iOSではASWebAuthenticationSession、AndroidではCustom Tabs)— 埋め込みWebViewは使用しない
  3. ブラウザでユーザーが認証
  4. ユニバーサルリンク/カスタムURLスキームでコールバックを受信
  5. トークンのcode + code_verifierを交換
  6. トークンを安全に保存(iOSではKeychain、AndroidではKeystore)

iOS例(Swift)

import CryptoKit

let verifier = Data(count: 32).map { _ in
    UInt8.random(in: 0...255)
}
let codeVerifier = Data(verifier).base64URLEncodedString()
let challenge = SHA256.hash(data: Data(codeVerifier.utf8))
let codeChallenge = Data(challenge).base64URLEncodedString()

Android例(Kotlin)

val verifier = ByteArray(32).also { SecureRandom().nextBytes(it) }
val codeVerifier = Base64.encodeToString(verifier, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)
val digest = MessageDigest.getInstance("SHA-256").digest(codeVerifier.toByteArray())
val codeChallenge = Base64.encodeToString(digest, Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP)

セキュリティベストプラクティス

  • システムブラウザを使用し、埋め込みWebViewは使用しない(資格情報を傍受可能)
  • カスタムURLスキームよりもユニバーサル/App Linksを優先
  • プラットフォームのセキュアストレージ(Keychain/Keystore)にトークンを保存
  • トークンエンドポイントに証明書ピンニングを実装
  • 認可リクエストごとに新しいPKCEペアを使用

ユースケース

IDプロバイダー(Google Sign-In、Azure AD B2Cなど)を介してユーザーを認証するiOSまたはAndroidアプリ。アプリがPKCEペアを生成し、システムブラウザをログイン用に開き、ユニバーサルリンクで認可コードを受信してトークンに交換します。

試してみる — OAuth 2.0 Flow Visualizer

フルツールを開く