Este documento descreve como integrar o Gerenciador de credenciais a um app Android que usa a WebView.
Visão geral
Antes de se aprofundar no processo de integração, é importante entender o fluxo de comunicação entre o código nativo do Android, um componente da Web renderizado em uma WebView que gerencia a autenticação do app e um back-end. O fluxo envolve o registro (criação de credenciais) e a autenticação (recebimento de credenciais existentes).
Registro (criar uma chave de acesso)
- O back-end gera o arquivo JSON de registro inicial e o envia para a página da Web renderizada na WebView.
- A página da Web usa
navigator.credentials.create()
para registrar novas credenciais. Você vai usar o JavaScript injetado para substituir esse método em uma etapa posterior para enviar a solicitação ao app Android. - O app Android usa o Gerenciador de credenciais para criar a solicitação de credencial e usá-la para
createCredential
. - O Gerenciador de credenciais compartilha a credencial de chave pública com o app.
- O app envia a credencial de chave pública de volta à página da Web para que o JavaScript injetado possa analisar as respostas.
- A página da Web envia a chave pública para o back-end, que a verifica e salva.

Autenticação (receber uma chave de acesso)
- O back-end gera um arquivo JSON de autenticação para receber a credencial e o envia para a página da Web renderizada no cliente da WebView.
- A página da Web usa
navigator.credentials.get
. Use o JavaScript injetado para substituir esse método para redirecionar a solicitação para o app Android. - O app recupera a credencial ao chamar
getCredential
no Gerenciador de credenciais. - O Gerenciador de credenciais retorna a credencial ao app.
- O app recebe a assinatura digital da chave privada e a envia à página da Web para que o JavaScript injetado possa analisar as respostas.
- Em seguida, a página da Web a envia ao servidor, que verifica a assinatura digital com a chave pública.

O mesmo fluxo pode ser usado para senhas ou sistemas de identidade federados.
Pré-requisitos
Para usar o Gerenciador de credenciais, conclua as etapas descritas na seção de pré-requisitos do guia da API e faça o seguinte:
- Adicione as dependências necessárias.
- Preserve as classes no arquivo do ProGuard.
- Adicione suporte ao Digital Asset Links.
Comunicação com JavaScript
Para permitir que o JavaScript em uma WebView e o código nativo do Android se comuniquem entre si, envie mensagens e processe solicitações entre os dois ambientes. Para fazer isso, injete um código JavaScript personalizado em uma WebView. Isso permite modificar o comportamento do conteúdo da Web e interagir com o código nativo do Android.
Injeção de JavaScript
O código JavaScript a seguir estabelece a comunicação entre a WebView e o app Android. Ele substitui os métodos navigator.credentials.create()
e navigator.credentials.get()
usados pela API WebAuthn para os fluxos de registro e autenticação descritos anteriormente.
Use a versão reduzida desse código JavaScript no seu aplicativo.
Criar um listener para chaves de acesso
Configure uma classe PasskeyWebListener
para processar a comunicação com o JavaScript. Ela precisa herdar de WebViewCompat.WebMessageListener
. Essa classe recebe mensagens do JavaScript e executa as ações necessárias no app Android.
Kotlin
// The class talking to Javascript should inherit: class PasskeyWebListener( private val activity: Activity, private val coroutineScope: CoroutineScope, private val credentialManagerHandler: CredentialManagerHandler ) : WebViewCompat.WebMessageListener // ... Implementation details
Java
// The class talking to Javascript should inherit: class PasskeyWebListener implements WebViewCompat.WebMessageListener { // Implementation details private Activity activity; // Handles get/create methods meant for Java: private CredentialManagerHandler credentialManagerHandler; public PasskeyWebListener( Activity activity, CredentialManagerHandler credentialManagerHandler ) { this.activity = activity; this.credentialManagerHandler = credentialManagerHandler; } // ... Implementation details }
Dentro de PasskeyWebListener
, implemente a lógica para solicitações e respostas, conforme descrito nas seções a seguir.
Processar a solicitação de autenticação
Para processar solicitações de operações navigator.credentials.create()
ou navigator.credentials.get()
da WebAuthn, o método onPostMessage
da classe PasskeyWebListener
é chamado quando o código JavaScript envia uma mensagem ao app Android:
Kotlin
class PasskeyWebListener(...)... { // ... /** havePendingRequest is true if there is an outstanding WebAuthn request. There is only ever one request outstanding at a time. */ private var havePendingRequest = false /** pendingRequestIsDoomed is true if the WebView has navigated since starting a request. The FIDO module cannot be canceled, but the response will never be delivered in this case. */ private var pendingRequestIsDoomed = false /** replyChannel is the port that the page is listening for a response on. It is valid if havePendingRequest is true. */ private var replyChannel: ReplyChannel? = null /** * Called by the page during a WebAuthn request. * * @param view Creates the WebView. * @param message The message sent from the client using injected JavaScript. * @param sourceOrigin The origin of the HTTPS request. Should not be null. * @param isMainFrame Should be set to true. Embedded frames are not supported. * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in the Channel. * @return The message response. */ @UiThread override fun onPostMessage( view: WebView, message: WebMessageCompat, sourceOrigin: Uri, isMainFrame: Boolean, replyProxy: JavaScriptReplyProxy, ) { val messageData = message.data ?: return onRequest( messageData, sourceOrigin, isMainFrame, JavaScriptReplyChannel(replyProxy) ) } private fun onRequest( msg: String, sourceOrigin: Uri, isMainFrame: Boolean, reply: ReplyChannel, ) { msg?.let { val jsonObj = JSONObject(msg); val type = jsonObj.getString(TYPE_KEY) val message = jsonObj.getString(REQUEST_KEY) if (havePendingRequest) { postErrorMessage(reply, "The request already in progress", type) return } replyChannel = reply if (!isMainFrame) { reportFailure("Requests from subframes are not supported", type) return } val originScheme = sourceOrigin.scheme if (originScheme == null || originScheme.lowercase() != "https") { reportFailure("WebAuthn not permitted for current URL", type) return } // Verify that origin belongs to your website, // it's because the unknown origin may gain credential info. if (isUnknownOrigin(originScheme)) { return } havePendingRequest = true pendingRequestIsDoomed = false // Use a temporary "replyCurrent" variable to send the data back, while // resetting the main "replyChannel" variable to null so it’s ready for // the next request. val replyCurrent = replyChannel if (replyCurrent == null) { Log.i(TAG, "The reply channel was null, cannot continue") return; } when (type) { CREATE_UNIQUE_KEY -> this.coroutineScope.launch { handleCreateFlow(credentialManagerHandler, message, replyCurrent) } GET_UNIQUE_KEY -> this.coroutineScope.launch { handleGetFlow(credentialManagerHandler, message, replyCurrent) } else -> Log.i(TAG, "Incorrect request json") } } } private suspend fun handleCreateFlow( credentialManagerHandler: CredentialManagerHandler, message: String, reply: ReplyChannel, ) { try { havePendingRequest = false pendingRequestIsDoomed = false val response = credentialManagerHandler.createPasskey(message) val successArray = ArrayList<Any>(); successArray.add("success"); successArray.add(JSONObject(response.registrationResponseJson)); successArray.add(CREATE_UNIQUE_KEY); reply.send(JSONArray(successArray).toString()) replyChannel = null // setting initial replyChannel for the next request } catch (e: CreateCredentialException) { reportFailure( "Error: ${e.errorMessage} w type: ${e.type} w obj: $e", CREATE_UNIQUE_KEY ) } catch (t: Throwable) { reportFailure("Error: ${t.message}", CREATE_UNIQUE_KEY) } } companion object { const val TYPE_KEY = "type" const val REQUEST_KEY = "request" const val CREATE_UNIQUE_KEY = "create" const val GET_UNIQUE_KEY = "get" } }
Java
class PasskeyWebListener implements ... { // ... /** * Called by the page during a WebAuthn request. * * @param view Creates the WebView. * @param message The message sent from the client using injected JavaScript. * @param sourceOrigin The origin of the HTTPS request. Should not be null. * @param isMainFrame Should be set to true. Embedded frames are not supported. * @param replyProxy Passed in by JavaScript. Allows replying when wrapped in the Channel. * @return The message response. */ @UiThread public void onPostMessage( @NonNull WebView view, @NonNull WebMessageCompat message, @NonNull Uri sourceOrigin, Boolean isMainFrame, @NonNull JavaScriptReplyProxy replyProxy, ) { if (messageData == null) { return; } onRequest( messageData, sourceOrigin, isMainFrame, JavaScriptReplyChannel(replyProxy) ) } private void onRequest( String msg, Uri sourceOrigin, boolean isMainFrame, ReplyChannel reply ) { if (msg != null) { try { JSONObject jsonObj = new JSONObject(msg); String type = jsonObj.getString(TYPE_KEY); String message = jsonObj.getString(REQUEST_KEY); boolean isCreate = type.equals(CREATE_UNIQUE_KEY); boolean isGet = type.equals(GET_UNIQUE_KEY); if (havePendingRequest) { postErrorMessage(reply, "The request already in progress", type); return; } replyChannel = reply; if (!isMainFrame) { reportFailure("Requests from subframes are not supported", type); return; } String originScheme = sourceOrigin.getScheme(); if (originScheme == null || !originScheme.toLowerCase().equals("https")) { reportFailure("WebAuthn not permitted for current URL", type); return; } // Verify that origin belongs to your website, // Requests of unknown origin may gain access to credential info. if (isUnknownOrigin(originScheme)) { return; } havePendingRequest = true; pendingRequestIsDoomed = false; // Use a temporary "replyCurrent" variable to send the data back, // while resetting the main "replyChannel" variable to null so it’s // ready for the next request. ReplyChannel replyCurrent = replyChannel; if (replyCurrent == null) { Log.i(TAG, "The reply channel was null, cannot continue"); return; } if (isCreate) { handleCreateFlow(credentialManagerHandler, message, replyCurrent)); } else if (isGet) { handleGetFlow(credentialManagerHandler, message, replyCurrent)); } else { Log.i(TAG, "Incorrect request json"); } } catch (JSONException e) { e.printStackTrace(); } } } }
Para handleCreateFlow
e handleGetFlow
, consulte o exemplo no GitHub (em inglês).
Processar a resposta
Para gerenciar as respostas enviadas do app nativo à página da Web, adicione JavaScriptReplyProxy
ao JavaScriptReplyChannel
.
Kotlin
class PasskeyWebListener(...)... { // ... // The setup for the reply channel allows communication with JavaScript. private class JavaScriptReplyChannel(private val reply: JavaScriptReplyProxy) : ReplyChannel { override fun send(message: String?) { try { reply.postMessage(message!!) } catch (t: Throwable) { Log.i(TAG, "Reply failure due to: " + t.message); } } } // ReplyChannel is the interface where replies to the embedded site are // sent. This allows for testing since AndroidX bans mocking its objects. interface ReplyChannel { fun send(message: String?) } }
Java
class PasskeyWebListener implements ... { // ... // The setup for the reply channel allows communication with JavaScript. private static class JavaScriptReplyChannel implements ReplyChannel { private final JavaScriptReplyProxy reply; JavaScriptReplyChannel(JavaScriptReplyProxy reply) { this.reply = reply; } @Override public void send(String message) { reply.postMessage(message); } } // ReplyChannel is the interface where replies to the embedded site are // sent. This allows for testing since AndroidX bans mocking its objects. interface ReplyChannel { void send(String message); } }
Capture todos os erros do app nativo e envie-os de volta para o lado do JavaScript.
Integrar com a WebView
Esta seção descreve como configurar sua integração com a WebView.
Inicializar a WebView
Na atividade do app Android, inicialize uma WebView
e configure um WebViewClient
complementar. O WebViewClient
processa a comunicação com o código JavaScript injetado na WebView
.
Configure a WebView e chame o Gerenciador de credenciais:
Kotlin
val credentialManagerHandler = CredentialManagerHandler(this) // ... AndroidView(factory = { WebView(it).apply { settings.javaScriptEnabled = true // Test URL: val url = "https://credman-web-test.glitch.me/" val listenerSupported = WebViewFeature.isFeatureSupported( WebViewFeature.WEB_MESSAGE_LISTENER ) if (listenerSupported) { // Inject local JavaScript that calls Credential Manager. hookWebAuthnWithListener(this, this@MainActivity, coroutineScope, credentialManagerHandler) } else { // Fallback routine for unsupported API levels. } loadUrl(url) } } )
Java
// Example shown in the onCreate method of an Activity @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); WebView webView = findViewById(R.id.web_view); // Test URL: String url = "https://credman-web-test.glitch.me/"; Boolean listenerSupported = WebViewFeature.isFeatureSupported( WebViewFeature.WEB_MESSAGE_LISTENER ); if (listenerSupported) { // Inject local JavaScript that calls Credential Manager. hookWebAuthnWithListener(webView, this, coroutineScope, credentialManagerHandler) } else { // Fallback routine for unsupported API levels. } webView.loadUrl(url); }
Crie um novo objeto de cliente da WebView e injete o código JavaScript na página da Web:
Kotlin
// This is an example call into hookWebAuthnWithListener val passkeyWebListener = PasskeyWebListener( activity, coroutineScope, credentialManagerHandler ) val webViewClient = object : WebViewClient() { override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { super.onPageStarted(view, url, favicon) // Handle page load events passkeyWebListener.onPageStarted(); webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null) } } webView.webViewClient = webViewClient
Java
// This is an example call into hookWebAuthnWithListener PasskeyWebListener passkeyWebListener = new PasskeyWebListener( activity, credentialManagerHandler ) WebViewClient webiewClient = new WebViewClient() { @Override public void onPageStarted(WebView view, String url, Bitmap favicon) { super.onPageStarted(view, url, favicon); // Handle page load events passkeyWebListener.onPageStarted(); webView.evaulateJavascript(PasskeyWebListener.INJECTED_VAL, null); } }; webView.setWebViewClient(webViewClient);
Configurar um listener de mensagens da Web
Para permitir que mensagens sejam postadas entre o JavaScript e o app Android, configure um listener de mensagens da Web com o método WebViewCompat.addWebMessageListener
.
Kotlin
val rules = setOf("*") if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener ) }
Java
Set<String> rules = new HashSet<>(Arrays.asList("*")); if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) { WebViewCompat.addWebMessageListener( webView, PasskeyWebListener.INTERFACE_NAME, rules, passkeyWebListener ) }
Integração com a Web
Para aprender a criar integração com a Web, confira as seções Criar uma chave de acesso para logins sem senha e Fazer login com uma chave de acesso usando o preenchimento automático de formulários.
Teste e implantação
Teste todo o fluxo em um ambiente controlado para garantir uma comunicação adequada entre o app Android, a página da Web e o back-end.
Implante a solução integrada na produção, garantindo que o back-end possa processar as solicitações de registro e autenticação. O código de back-end precisa gerar o arquivo JSON inicial para os processos de registro (criação) e autenticação (recebimento). Ele também precisa processar a validação e verificação das respostas recebidas da página da Web.
Verifique se a implementação corresponde às recomendações de UX.
Observações importantes
- Use o código JavaScript fornecido para processar as operações
navigator.credentials.create()
enavigator.credentials.get()
. - A classe
PasskeyWebListener
é a ponte entre o app Android e o código JavaScript na WebView. Ela processa a transmissão de mensagens, a comunicação e a execução das ações necessárias. - Adapte os snippets de código fornecidos à estrutura, às convenções de nomenclatura e aos requisitos específicos do projeto.
- Detecte erros no app nativo e envie-os de volta para o lado do JavaScript.
Ao seguir este guia e integrar o Gerenciador de credenciais ao seu app Android que usa a WebView, você pode oferecer aos usuários uma experiência de login com chaves de acesso segura e integrada, bem como gerenciar as credenciais de maneira eficaz.