アプリ開発備忘録

PlayStationMobile、Android、UWPの開発備忘録

FIDOログインをAndroid-Backendで実装する時のハマりどころ

バックエンドはWebaAthn4jを使用しています。

Androidのパスワード系は軒並みDeprecateになっていて、今は androidx.credentials 一択になっているようです。
androidx.credentials:credentials-play-services-auth
androidx.credentials:credentials

/.well-known/assetlinks.json をサーバーに置きます。DeepLink等でおなじみですが、delegate_permission/common.get_login_credsを追加する必要があります。尚、delegate_permission/common.handle_all_urlsの方も無いとエラーになります。
Googleのマネージャーだと androidx.credentials.exceptions.publickeycredential.GetPublicKeyCredentialDomException: The incoming request cannot be validated
Bitwadenだとユーザーがキャンセルした扱いになります。

[
  {
    "relation": [
      "delegate_permission/common.handle_all_urls",
      "delegate_permission/common.get_login_creds"
    ],
    "target": {
      "namespace": "android_app",
      "package_name": "${packageName}",
      "sha256_cert_fingerprints": [
        "${appFingerprint}"
      ]
    }
  }
]

WebAuthn4J側の設定では検証時にandroid:apk-key-hash:の設定が必要になります。
実際にエラーを起こしてhashを見るのが手っ取り早いですが、以下でも取得できます。

keytool -exportcert -keystore <keystore> -alias <alias> -storepass <storepass> -keypass <keypass> | openssl sha256 -binary | openssl base64 | tr -- '+/' '-_' | tr -d '='
ServerProperty(
    setOf(
        Origin("https://example.com"),
        Origin("android:apk-key-hash:${apkKeyHash}"),
    ),
    "example.com",
    { challenge.toByteArray() },
    null,
)

Android側でリクエストを組み立てる時に、challengeはBase64エンコードしないと検証時にinvalidになりました。

val getPublicKeyCredentialOption = GetPublicKeyCredentialOption(
    """
        {
            "challenge": "${Base64.getEncoder().encodeToString(challenge.toByteArray())}",
            "rpId": "$domain",
            "userVerification": "required",
            "timeout": 1800000
        }
    """.trimIndent(),
    null,
)

Androidで取得できたresponseのauthenticatorDataclientDataJSONsignatureuserHandleは全てUrlBase64な為、通常のBase64に直す必要がありました。

private fun String?.base64UrlToBase64(): String? {
    this ?: return null
    return Base64.getEncoder()
        .encode(
            Base64.getUrlDecoder()
                .decode(this),
        )
        .decodeToString()
}

おわり

情報があまり無くて大変でしたが、なんとか実装できました。