Migrar do FIDO2 para o Gerenciador de credenciais

Com suporte para chaves de acesso, login federado e provedores de autenticação de terceiros, o Gerenciador de credenciais é a API recomendada para autenticação no Android que oferece um ambiente seguro e conveniente que permite aos usuários sincronizar e gerenciar as próprias credenciais. Para desenvolvedores que usam credenciais do FIDO2 locais, atualize seu app para oferecer suporte à autenticação de chaves de acesso pela integração com a API Credential Manager. Este documento descreve como migrar seu projeto do FIDO2 para o Gerenciador de credenciais.

Motivos para migrar do FIDO2 para o Gerenciador de credenciais

Na maioria dos casos, você precisa migrar o provedor de autenticação do seu app Android para o Gerenciador de credenciais: Os motivos para essa migração incluem:

  • Suporte a chaves de acesso: o Gerenciador de credenciais oferece suporte a chaves de acesso, um novo mecanismo de autenticação sem senha que é mais seguro e fácil de usar do que senhas.
  • Vários métodos de login: o Gerenciador de credenciais oferece suporte a vários métodos de login, incluindo senhas, chaves de acesso e métodos de login federados. Isso facilita a autenticação dos usuários no seu app, independente do método de autenticação preferido.
  • Suporte a provedores de credenciais de terceiros: no Android 14 e versões mais recentes, o Gerenciador de credenciais oferece suporte a vários provedores de credenciais de terceiros. Isso significa que seus usuários podem usar as credenciais atuais de outros provedores para fazer login no seu app.
  • Experiência do usuário consistente:o Gerenciador de credenciais oferece uma experiência do usuário mais consistente para autenticação em todos os apps e mecanismos de login. Isso facilita que os usuários entendam e usem o fluxo de autenticação do app.

Para iniciar a migração do FIDO2 para o Gerenciador de credenciais, siga as etapas abaixo.

Atualizar dependências

  1. Atualize o plug-in do Kotlin no build.gradle do projeto para a versão 1.8.10 ou mais recente.

      plugins {
        //…
          id 'org.jetbrains.kotlin.android' version '1.8.10' apply false
        //…
      }
    
  2. No build.gradle do projeto, atualize as dependências para usar as versões mais recentes das bibliotecas do Gerenciador de credenciais e da autenticação do Google Play Services.

      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. Substitua a inicialização do FIDO pela inicialização do Gerenciador de credenciais. Adicione essa declaração na classe que você usa para criação de chaves de acesso e métodos de login:

    val credMan = CredentialManager.create(context)
    

Criar chaves de acesso

Você precisará criar uma nova chave de acesso, associá-la à conta de um usuário e armazenar a chave pública no servidor para que o usuário possa fazer login com ela. Configure seu app com esse recurso atualizando as chamadas de função de registro.

Figura 1. Esta figura mostra como os dados são trocados entre o app e o servidor quando uma chave de acesso é criada usando o Gerenciador de credenciais.
  1. Para acessar os parâmetros necessários que são enviados ao método createCredential() durante a criação da chave de acesso, adicione name("residentKey").value("required") (conforme descrito na especificação WebAuthn) à chamada de servidor registerRequest().

    suspend fun registerRequest() {
        // ...
        val call = client.newCall(
            Builder()
                .method("POST", jsonRequestBody {
                    name("attestation").value("none")
                    name("authenticatorSelection").objectValue {
                        name("residentKey").value("required")
                    }
            }).build()
        )
        // ...
    }
    
  2. Defina o tipo de return para registerRequest() e todas as funções filhas como JSONObject.

    suspend fun registerRequest(sessionId: String): ApiResult<JSONObject> {
        val call = client.newCall(
            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. Remova com segurança da sua visualização todos os métodos que processam chamadas de tela de início de intents e de resultados de atividades.

  4. Como registerRequest() agora retorna um JSONObject, não é necessário criar uma PendingIntent. Substitua a intent retornada por um JSONObject. Atualize as chamadas da tela de início da intent para chamar createCredential() na API Credential Manager. Chame o método de API createCredential().

    suspend fun createPasskey(
        activity: Activity,
        requestResult: JSONObject
    ): CreatePublicKeyCredentialResponse? {
        val request = CreatePublicKeyCredentialRequest(requestResult.toString())
        var response: CreatePublicKeyCredentialResponse? = null
        try {
            response = credMan.createCredential(
                request = request as CreateCredentialRequest,
                context = activity
            ) as CreatePublicKeyCredentialResponse
        } catch (e: CreateCredentialException) {
    
            showErrorAlert(activity, e)
    
            return null
        }
        return response
    }
    
  5. Quando a chamada for concluída, envie a resposta de volta ao servidor. A solicitação e a resposta dessa chamada são semelhantes à implementação do FIDO2. Portanto, nenhuma mudança é necessária.

Autenticar com chaves de acesso

Depois de configurar a criação da chave de acesso, você pode configurar seu app para permitir que os usuários façam login e se autentiquem usando as chaves de acesso. Para fazer isso, atualize o código de autenticação para processar os resultados do Gerenciador de credenciais e implemente uma função para autenticar usando chaves de acesso.

Figura 2. Fluxo de autenticação de chaves de acesso do Gerenciador de credenciais.
  1. A chamada de solicitação de login ao servidor para receber as informações necessárias a serem enviadas à solicitação getCredential() é a mesma que a implementação do FIDO2. Nenhuma mudança é necessária.
  2. Semelhante à chamada de solicitação de registro, a resposta retornada está no formato JSONObject.

    /**
     * @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. Remova com segurança qualquer método que processe a tela de início de intents e chamadas de resultado de atividades da sua visualização.

  4. Como signInRequest() agora retorna um JSONObject, não é necessário criar uma PendingIntent. Substitua a intent retornada por um JSONObject e chame getCredential() nos métodos da API.

    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. Depois que a chamada for bem-sucedida, envie a resposta de volta ao servidor para validar e autenticar o usuário. Os parâmetros de solicitação e resposta para essa chamada de API são semelhantes à implementação do FIDO2. Portanto, nenhuma mudança é necessária.

Outros recursos