Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Zero-Tap Sign-In対応、結局何をすればいいの?

Zero-Tap Sign-In対応、結局何をすればいいの?

Avatar for mitohato14

mitohato14

October 23, 2026

More Decks by mitohato14

Other Decks in Programming

Transcript

  1. パスキーと同じところ 登録・署名・検証の流れ 鍵を作るときのパラメーターもほぼ同じ 01 登録 鍵ペアを作り 公開鍵をサーバーへ登録 DMM App Night

    #2 #dmm_app_night 02 署名 → サーバーのチャレンジに 秘密鍵で署名 03 検証 → サーバーが 公開鍵で検証 11
  2. パスキーと同じところ 登録・署名・検証の流れ 鍵を作るときのパラメーターもほぼ同じ 01 登録 鍵ペアを作り 公開鍵をサーバーへ登録 DMM App Night

    #2 #dmm_app_night 02 署名 → サーバーのチャレンジに 秘密鍵で署名 03 検証 → サーバーが 公開鍵で検証 11
  3. パスキーと同じところ 登録・署名・検証の流れ 鍵を作るときのパラメーターもほぼ同じ 01 登録 鍵ペアを作り 公開鍵をサーバーへ登録 DMM App Night

    #2 #dmm_app_night 02 署名 → サーバーのチャレンジに 秘密鍵で署名 03 検証 → サーバーが 公開鍵で検証 11
  4. 具体的にどうしたらいい? 復元キーの処理は3つ 既存のログインに足す ① 作る ログイン・登録後 ログイン済みで未作成のときも DMM App Night

    #2 #dmm_app_night ② 使う 機種変更後の起動時 復元キーでログイン ③ 消す ログアウト時 アカウント削除時 セッション失効時 13
  5. ① ログイン・登録時 / アプリ ログイン成功後に裏で作る ログイン中のユーザーで開始 val options = api.startRegistration()

    // val request = CreateRestoreCredentialRequest( requestJson = options, // publicKey isCloudBackupEnabled = true // E2EE ) val response = credentialManager.createCredential(context, request) as CreateRestoreCredentialResponse api.finishRegistration(response.responseJson) // ラッパーの中身 でクラウドにバックアップ 公開鍵を登録 ユーザーには確認しない 失敗してもログイン処理続行 DMM App Night #2 #dmm_app_night 15
  6. ① ログイン・登録時 / アプリ E2EEが使えなければ端末内に作る val response = try {

    createRestoreCredential(options, isCloudBackupEnabled = true) } catch (e: E2eeUnavailableException) { // createRestoreCredential(options, isCloudBackupEnabled = false) } 画面ロックやバックアップが無効なとき 端末内の鍵はクラウドからの復元では使えない DMM App Night #2 #dmm_app_night 16
  7. ① ログイン・登録時 / アプリ ログイン済みの人にも の など // MainActivity onCreate

    if (isSignedIn && !prefs.hasSyncedRestoreCredential) { authRepository.createRestoreCredential() .onSuccess { prefs.hasSyncedRestoreCredential = true } } アップデート前からログインしている人はログイン画面を通らない DMM App Night #2 #dmm_app_night 17
  8. ① ログイン・登録時 / バックエンド パスキーの登録と同じ 01 チャレンジを発行 ログイン中のセッションで始める user.idは個人情報を含まない値 DMM

    App Night #2 #dmm_app_night 02 検証して公開鍵を保存 → パスキー一覧に出さないよう パスキーとは区別して保存 18
  9. ① ログイン・登録時 / バックエンド パスキーの登録と同じ 01 チャレンジを発行 ログイン中のセッションで始める user.idは個人情報を含まない値 DMM

    App Night #2 #dmm_app_night 02 検証して公開鍵を保存 → パスキー一覧に出さないよう パスキーとは区別して保存 18
  10. ② 機種変更後の起動時 / アプリ まずは復元キーを確認 ユーザー名なし val options = api.startAuthentication()

    // val request = GetCredentialRequest( listOf(GetRestoreCredentialOption(options)) // Option ) val credential = credentialManager.getCredential(context, request) .credential as RestoreCredential api.finishAuthentication(credential.authenticationResponseJson) 他の と混在不可 未ログイン時に実行 失敗したら通常のログイン画面へ DMM App Night #2 #dmm_app_night 20
  11. ② 機種変更後の起動時 / バックエンド 復元キーからユーザーを特定 01 チャレンジを発行 ユーザーを指定せずに発行 DMM App

    Night #2 #dmm_app_night 02 ユーザーを特定 → 署名と一緒に届く userHandleで探す 03 検証してログイン → 公開鍵で検証して セッションを発行 21
  12. ② 機種変更後の起動時 / バックエンド 復元キーからユーザーを特定 01 チャレンジを発行 ユーザーを指定せずに発行 DMM App

    Night #2 #dmm_app_night 02 ユーザーを特定 → 署名と一緒に届く userHandleで探す 03 検証してログイン → 公開鍵で検証して セッションを発行 21
  13. ② 機種変更後の起動時 / バックエンド 復元キーからユーザーを特定 01 チャレンジを発行 ユーザーを指定せずに発行 DMM App

    Night #2 #dmm_app_night 02 ユーザーを特定 → 署名と一緒に届く userHandleで探す 03 検証してログイン → 公開鍵で検証して セッションを発行 21
  14. ③ ログアウト時 タイプを指定して消す ログアウト・アカウント削除・サーバーでセッション失効を確認したとき 引数なしだと 復元キーが消えない // // val request

    = ClearCredentialStateRequest( ClearCredentialStateRequest.TYPE_CLEAR_RESTORE_CREDENTIAL ) credentialManager.clearCredentialState(request) 消し忘れるとログアウトしたはずのアカウントで新端末に自動ログインされる DMM App Night #2 #dmm_app_night 23
  15. 詰まったポイント publicKeyで包まれていると失敗 ライブラリの出力 Credential Manager {"publicKey": {"challenge": …}} {"challenge": …,

    "user": …} ブラウザのAPIにそのまま渡す形 包まれたまま渡すと 「user.id must be defined」 包まれている 中身だけ アプリかサーバーでpublicKeyの中身を取り出す DMM App Night #2 #dmm_app_night 25
  16. 詰まったポイント Originに入る値がWebと違う Web のパスキー サイトのURL Android アプリ(復元キー・パスキー) アプリの署名証明書 https://example.com android:apk-key-hash:<SHA-256>

    Webだけならこれを許可していればよい WebのURLだけ許可していると 「Incorrect origin」で400 サーバーの許可リストにAndroidの値も追加する DMM App Night #2 #dmm_app_night 26
  17. 進め方 バックエンドメンバーを巻き込もう アプリだけでは完結しない WebAuthnサーバー パスキー導入済みなら 既存の仕組みを流用できる DMM App Night #2

    #dmm_app_night AndroidのOrigin APKキーハッシュを 許可リストに追加 パスキーと区別 パスキー管理画面には 出さない 28
  18. おまけ アプリを開く前にアカウント移行が完了 [ 新端末のセットアップ ] ↓ アプリのデータを復元 【 onRestoreFinished() 】

    ← ここで 復元キーを使う ↓ [ ユーザーがアプリを開く ] → ログイン済み まずは初回起動時の対応から BackupAgentは余裕があれば足す DMM App Night #2 #dmm_app_night 31
  19. おまけ onRestoreFinishedで復元キーを使う class AppBackupAgent : BackupAgent() { override fun onRestoreFinished()

    { runBlocking { // authRepository.signInWithRestoreCredential() } } } 終わるまで待つ onRestoreではなくonRestoreFinishedを使う DMM App Night #2 #dmm_app_night 33
  20. 付録 実装チェックリスト アプリ ログイン時とログイン済みで未作成なら作る E2EEが使えなければ端末内に作り直す 未ログインの初回起動で取得 復元できたらFCMトークンも送り直す ログアウト・アカウント削除・セッション失効で 消す バックエンド

    ログイン中のセッションで登録 user.idは個人情報を含まない値 パスキーとは区別して保存 AndroidのOriginを許可 チャレンジは使い終わったら捨てる 確認はAndroid StudioのBackup / Restore App Data DMM App Night #2 #dmm_app_night 35
  21. 付録 動作条件 復元キーは1アプリにつき1アカウントまで Android 9以上 APIレベル28 DMM App Night #2

    #dmm_app_night GMS 24220000以上 androidx.credentials 1.5.0以上 credentials-play-servicesauthも追加 36
  22. 付録 対象と例外 復元キーを取得できたかで判定される 対象外 ゲーム スマホとタブレット以外 DMM App Night #2

    #dmm_app_night 対応済み扱い Block Storeを 2026/9/30までに本番導入 例外申請 金融や医療など規制がある 施行前にPlay Consoleから 37
  23. 付録 / 動作確認 Android Studioだけで確認できる 01 バックアップ ログインして Backup App

    Data DMM App Night #2 #dmm_app_night 02 入れ直す → アンインストールして 再インストール 03 復元 → Restore App Data 開くとログイン済み 38
  24. 付録 / 動作確認 Android Studioだけで確認できる 01 バックアップ ログインして Backup App

    Data DMM App Night #2 #dmm_app_night 02 入れ直す → アンインストールして 再インストール 03 復元 → Restore App Data 開くとログイン済み 38
  25. 付録 / 動作確認 Android Studioだけで確認できる 01 バックアップ ログインして Backup App

    Data DMM App Night #2 #dmm_app_night 02 入れ直す → アンインストールして 再インストール 03 復元 → Restore App Data 開くとログイン済み 38
  26. 付録 / Origin APKキーハッシュの出し方 配布APKの署名証明書のSHA-256を取得 Play App Signing:Play Consoleの「アプリ署名鍵の証明書」 自分でAPKに署名:keytool

    -list -v -keystore <署名用keystore> 2. SHA-256のhexをバイト列に戻してBase64URL化(末尾の = は削る) 3. 先頭に android:apk-key-hash: を付ける 1. サーバーの「Incorrect origin」のログにも出る デバッグ用と配布版の両方を許可(アップロード鍵と区別) DMM App Night #2 #dmm_app_night 39
  27. 付録 / やりとりする値 アプリとBEのやりとり 【① 作る】 アプリ → BE POST

    /register/start アプリ ← BE 登録オプション(challenge・rp・user) アプリ → BE POST /register/finish(公開鍵) 【② 使う】 アプリ → BE POST /authenticate/start(ユーザー指定なし) アプリ ← BE 認証オプション(challenge・rpId) アプリ → BE POST /authenticate/finish(署名・userHandle) 値はJSONのまま受け渡すだけ DMM App Night #2 #dmm_app_night 40
  28. 付録 / やりとりする値 登録オプション(BE → アプリ) { "challenge": "Xb3k…", "rp":

    { "id": "example.com", "name": "Example" }, "user": { "id": "q2Hj…", "name": "[email protected]", … }, "pubKeyCredParams": [{ "type": "public-key", "alg": -7 }, …] } user.idは個人情報を含まないランダムな値 DMM App Night #2 #dmm_app_night 41
  29. 付録 / やりとりする値 登録結果(アプリ → BE) { "id": "AbCd…", "type":

    "public-key", "response": { "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRl…", "attestationObject": "o2NmbXRkbm9uZ…" } } 公開鍵はattestationObjectの中 originはclientDataJSONの中 DMM App Night #2 #dmm_app_night 42
  30. 付録 / やりとりする値 認証オプション(BE → アプリ) { "challenge": "Pq9w…", "rpId":

    "example.com", "userVerification": "preferred" } allowCredentialsなし ユーザーを指定しない DMM App Night #2 #dmm_app_night 43
  31. 付録 / やりとりする値 認証結果(アプリ → BE) { "id": "AbCd…", "response":

    { "clientDataJSON": "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0…", "authenticatorData": "SZYN5YgOjGh0…", "signature": "MEUCIQ…", "userHandle": "q2Hj…" } } userHandleは登録時のuser.id DMM App Night #2 #dmm_app_night 44
  32. 参考資料 参考資料 認証情報の復元について: https://developer.android.com/identity/sign-in/restore-credentials 認証情報の復元を実装する: https://developer.android.com/identity/sign-in/restore-credentialsimplementation 認証情報の復元をテストする: https://developer.android.com/identity/sign-in/test-restore-credentials Play Consoleの技術的な品質要件(ゼロタップでのログインの復元):

    https://support.google.com/googleplay/android-developer/answer/17492799#zero-tap_sign-in_restoration アプリサーバーで公開鍵を検証して保存する(Origin): https://developer.android.com/identity/passkeys/create-passkeys#verify 認証情報マネージャーについて: https://developer.android.com/identity/credential-manager DMM App Night #2 #dmm_app_night 45