Autenticar usuários com a WebView

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)

  1. O back-end gera o arquivo JSON de registro inicial e o envia para a página da Web renderizada na WebView.
  2. 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.
  3. O app Android usa o Gerenciador de credenciais para criar a solicitação de credencial e usá-la para createCredential.
  4. O Gerenciador de credenciais compartilha a credencial de chave pública com o app.
  5. O app envia a credencial de chave pública de volta à página da Web para que o JavaScript injetado possa analisar as respostas.
  6. A página da Web envia a chave pública para o back-end, que a verifica e salva.
Gráfico mostrando o fluxo de registro da chave de acesso
Figura 1. Fluxo de registro da chave de acesso.

Autenticação (receber uma chave de acesso)

  1. 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.
  2. 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.
  3. O app recupera a credencial ao chamar getCredential no Gerenciador de credenciais.
  4. O Gerenciador de credenciais retorna a credencial ao app.
  5. 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.
  6. Em seguida, a página da Web a envia ao servidor, que verifica a assinatura digital com a chave pública.
Gráfico mostrando o fluxo de autenticação da chave de acesso
Figura 2. Fluxo de autenticação da chave de acesso.

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:

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() e navigator.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.