Migrate from FIDO2 to Credential Manager

With support for passkeys, federated sign-in, and third-party authentication providers, Credential Manager is the recommended API for authentication on Android that provides a secure and convenient environment that allows users to sync and manage their credentials. For developers that use local FIDO2 credentials, you should update your app to support passkey authentication by integrating with the Credential Manager API. This document describes how to migrate your project from FIDO2 to Credential Manager.

Reasons to migrate from FIDO2 to Credential Manager

In most cases, you should migrate your Android app's authentication provider to Credential Manager. The reasons to migrate to Credential Manager include:

  • Passkey support: Credential Manager supports passkeys, a new, passwordless authentication mechanism that is more secure and easier to use than passwords.
  • Multiple sign-in methods: Credential Manager supports multiple sign-in methods, including passwords, passkeys, and federated sign-in methods. This makes it easier for users to authenticate to your app, regardless of their preferred authentication method.
  • Third party credential provider support: On Android 14 and higher, Credential Manager supports multiple third party credential providers. This means your users can use their existing credentials from other providers to sign in to your app.
  • Consistent user experience: Credential Manager provides a more consistent user experience for authentication across apps and sign-in mechanisms. This makes it easier for users to understand and use your app's authentication flow.

To begin migration from FIDO2 to Credential Manager, follow the steps below.

Update dependencies

  1. Update the Kotlin plugin in your project's build.gradle to version 1.8.10 or higher.

    plugins {
      //…
        id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
      //…
    }
    
  2. In your project's build.gradle, update your dependencies to use Credential Manager and Play Services Authentication.

    dependencies {
      // ...
      // Credential Manager:
      implementation 'androidx.credentials:credentials:<latest-version>'
    
      // Play Services Authentication:
      // Optional - needed for credentials support from play services, for devices running
      // Android 13 and below:
      implementation 'androidx.credentials:credentials-play-services-auth:<latest-version>'
      // ...
    }
    
  3. Replace FIDO initialization with Credential Manager initialization. Add this declaration in the class you use for passkey creation and sign in methods:

    val credMan = CredentialManager.create(context)
    

Create passkeys

You'll need to create a new passkey, associate it with a user's account, and store the passkey's public key on your server before the user can sign in with it. Set up your app with this ability by updating the register function calls.

Figure 1. This figure shows how data is exchanged between app and server when a passkey is created using Credential Manager.
  1. To obtain the necessary parameters that are sent to the createCredential() method during passkey creation, add name("residentKey").value("required") as described in the WebAuthn specification) to your registerRequest() server call.

    suspend fun registerRequest(sessionId: String ... {
        // ...
        .method("POST", jsonRequestBody {
            name("attestation").value("none")
            name("authenticatorSelection").objectValue {
                name("residentKey").value("required")
            }
        }).build()
        // ...
    }
    
  2. Set the return type for registerRequest() and all child functions to JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            Request.Builder()
                .url("$BASE_URL/<your api url>")
                .addHeader("Cookie", formatCookie(sessionId))
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("authenticatorAttachment").value("platform")
                        name("userVerification").value("required")
                        name("residentKey").value("required")
                    }
                }).build()
        )
        val response = call.await()
        return response.result("Error calling the api") {
            parsePublicKeyCredentialCreationOptions(
                body ?: throw ApiException("Empty response from the api call")
            )
        }
    }
    
  3. Safely remove any methods that handle intent launcher and activity result calls from your view.

  4. Since registerRequest() now returns a JSONObject, you don't need to create a PendingIntent. Replace the returned intent with a JSONObject. Update your intent launcher calls to call createCredential() from the Credential Manager API. Call createCredential() API method.

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
        ): CreatePublicKeyCredentialResponse? {
            val request = CreatePublicKeyCredentialRequest(requestResult.toString())
            var response: CreatePublicKeyCredentialResponse? = null
            try {
                response = credMan.createCredential(
                    request as CreateCredentialRequest,
                    activity
                ) as CreatePublicKeyCredentialResponse
            } catch (e: CreateCredentialException) {
    
                showErrorAlert(activity, e)
    
                return null
            }
            return response
        }
    
  5. Once the call is successful, send the response back to the server. The request and response for this call are similar to the FIDO2 implementation, so no changes are required.

Authenticate with passkeys

After you set up passkey creation, you can set up your app to allow users to sign in and authenticate using their passkeys. To do this, you'll update your authentication code to handle Credential Manager results, and implement a function to authenticate through passkeys.

Figure 2. Credential Manager's passkey authentication flow.
  1. Your sign in request call to the server to get necessary information to be sent to getCredential() request is the same as the FIDO2 implementation. No changes are required.
  2. Similar to the register request call, the returned response is in JSONObject format.

    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param credentialId The credential ID of this device.
     * @return a JSON object.
     */
    suspend fun signinRequest(): ApiResult<JSONObject> {
        val call = client.newCall(Builder().url(buildString {
            append("$BASE_URL/signinRequest")
        }).method("POST", jsonRequestBody {})
            .build()
        )
        val response = call.await()
        return response.result("Error calling /signinRequest") {
            parsePublicKeyCredentialRequestOptions(
                body ?: throw ApiException("Empty response from /signinRequest")
            )
        }
    }
    
    /**
     * @param sessionId The session ID to be used for the sign-in.
     * @param response The JSONObject for signInResponse.
     * @param credentialId id/rawId.
     * @return A list of all the credentials registered on the server,
     * including the newly-registered one.
     */
    suspend fun signinResponse(
        sessionId: String, response: JSONObject, credentialId: String
        ): ApiResult<Unit> {
    
            val call = client.newCall(
                Builder().url("$BASE_URL/signinResponse")
                    .addHeader("Cookie",formatCookie(sessionId))
                    .method("POST", jsonRequestBody {
                        name("id").value(credentialId)
                        name("type").value(PUBLIC_KEY.toString())
                        name("rawId").value(credentialId)
                        name("response").objectValue {
                            name("clientDataJSON").value(
                                response.getString("clientDataJSON")
                            )
                            name("authenticatorData").value(
                                response.getString("authenticatorData")
                            )
                            name("signature").value(
                                response.getString("signature")
                            )
                            name("userHandle").value(
                                response.getString("userHandle")
                            )
                        }
                    }).build()
            )
            val apiResponse = call.await()
            return apiResponse.result("Error calling /signingResponse") {
            }
        }
    
  3. Safely remove any methods that handle the intent launcher and activity result calls from your view.

  4. Since signInRequest() now returns a JSONObject, you don't need to create a PendingIntent. Replace the returned intent with a JSONObject, and call getCredential() from your API methods.

    suspend fun getPasskey(
        activity: Activity,
        creationResult: JSONObject
        ): GetCredentialResponse? {
            Toast.makeText(
                activity,
                "Fetching previously stored credentials",
                Toast.LENGTH_SHORT)
                .show()
            var result: GetCredentialResponse? = null
            try {
                val request= GetCredentialRequest(
                    listOf(
                        GetPublicKeyCredentialOption(
                            creationResult.toString(),
                            null
                        ),
                        GetPasswordOption()
                    )
                )
                result = credMan.getCredential(activity, request)
                if (result.credential is PublicKeyCredential) {
                    val publicKeycredential = result.credential as PublicKeyCredential
                    Log.i("TAG", "Passkey ${publicKeycredential.authenticationResponseJson}")
                    return result
                }
            } catch (e: Exception) {
                showErrorAlert(activity, e)
            }
            return result
        }
    
  5. Once the call is successful, send the response back to the server to validate and authenticate the user. The request and response parameters for this API call are similar to the FIDO2 implementation, so no changes are required.

Additional resources