Questo documento descrive come integrare l'API Credential Manager con un'app per Android che utilizza WebView.
Panoramica
Prima di addentrarci nel processo di integrazione, è importante comprendere il flusso di comunicazione tra il codice Android nativo, un componente web visualizzato all'interno di una WebView che gestisce l'autenticazione dell'app e un backend. Il flusso prevede la registrazione (creazione delle credenziali) e l'autenticazione (ottenimento delle credenziali esistenti).
Registrazione (creazione di una passkey)
- Il backend genera il JSON di registrazione iniziale e lo invia alla pagina web visualizzata all'interno di WebView.
- La pagina web utilizza
navigator.credentials.create()
per registrare nuove credenziali. Utilizzerai il codice JavaScript inserito per eseguire l'override di questo metodo in un passaggio successivo per inviare la richiesta all'app per Android. - L'app per Android utilizza l'API Credential Manager per creare la richiesta di credenziali e utilizzarla per
createCredential
. - L'API Credential Manager condivide la credenziale di chiave pubblica con l'app.
- L'app invia la credenziale della chiave pubblica alla pagina web in modo che il codice JavaScript inserito possa analizzare le risposte.
- La pagina web invia la chiave pubblica al backend, che la verifica e la salva.

Autenticazione (ottieni una passkey)
- Il backend genera un JSON di autenticazione per ottenere le credenziali e lo invia alla pagina web visualizzata nel client WebView.
- La pagina web utilizza
navigator.credentials.get
. Utilizza il codice JavaScript inserito per eseguire l'override di questo metodo e reindirizzare la richiesta all'app per Android. - L'app recupera la credenziale utilizzando l'API Credential Manager chiamando
getCredential
. - L'API Credential Manager restituisce la credenziale all'app.
- L'app ottiene la firma digitale della chiave privata e la invia alla pagina web in modo che il codice JavaScript inserito possa analizzare le risposte.
- La pagina web lo invia quindi al server che verifica la firma digitale con la chiave pubblica.

Lo stesso flusso potrebbe essere utilizzato per le password o i sistemi di identità federata.
Prerequisiti
Per utilizzare l'API Credential Manager, completa i passaggi descritti nella sezione Prerequisiti della guida di Credential Manager e assicurati di fare quanto segue:
- Aggiungi le dipendenze richieste.
- Conserva le classi nel file ProGuard.
- Aggiunto il supporto per i Digital Asset Links.
Comunicazione JavaScript
Per consentire a JavaScript in un WebView e al codice Android nativo di comunicare tra loro, devi inviare messaggi e gestire le richieste tra i due ambienti. Per farlo, inserisci codice JavaScript personalizzato in un componente WebView. Ciò ti consente di modificare il comportamento dei contenuti web e interagire con il codice Android nativo.
Inserimento di JavaScript
Il seguente codice JavaScript stabilisce la comunicazione
tra WebView e l'app per Android. Esegue l'override dei metodi
navigator.credentials.create()
e
navigator.credentials.get()
utilizzati dall'API WebAuthn per i flussi di registrazione e autenticazione
descritti in precedenza.
Utilizza la versione ridotta di questo codice JavaScript nella tua applicazione.
Crea un listener per le passkey
Configura una classe PasskeyWebListener
che gestisce la comunicazione
con JavaScript. Questa classe deve ereditare da
WebViewCompat.WebMessageListener
. Questa classe riceve messaggi da
JavaScript ed esegue le azioni necessarie nell'app per Android.
Le sezioni seguenti descrivono la struttura della classe PasskeyWebListener
,
nonché la gestione di richieste e risposte.
Gestire la richiesta di autenticazione
Per gestire le richieste per le operazioni WebAuthn navigator.credentials.create()
o
navigator.credentials.get()
, il metodo onPostMessage
della classe
PasskeyWebListener
viene chiamato quando il codice JavaScript invia un messaggio all'app Android:
// The class talking to Javascript should inherit:
class PasskeyWebListener(
private val activity: Activity,
private val coroutineScope: CoroutineScope,
private val credentialManagerHandler: CredentialManagerHandler
) : WebViewCompat.WebMessageListener {
/** 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 {
/** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */
const val INTERFACE_NAME = "__webauthn_interface__"
const val TYPE_KEY = "type"
const val REQUEST_KEY = "request"
const val CREATE_UNIQUE_KEY = "create"
const val GET_UNIQUE_KEY = "get"
/** INJECTED_VAL is the minified version of the JavaScript code described at this class
* heading. The non minified form is found at credmanweb/javascript/encode.js.*/
const val INJECTED_VAL = """
var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)};
"""
}
Per handleCreateFlow
e handleGetFlow
, consulta l'esempio su GitHub.
Gestire la risposta
Per gestire le risposte inviate dall'app nativa alla pagina web, aggiungi
JavaScriptReplyProxy
all'interno di JavaScriptReplyChannel
.
// 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?)
}
Assicurati di rilevare eventuali errori dell'app nativa e di inviarli al lato JavaScript.
Eseguire l'integrazione con WebView
Questa sezione descrive come configurare l'integrazione di WebView.
Inizializza WebView
Nell'attività della tua app per Android, inizializza un WebView
e configura un
WebViewClient
di accompagnamento. WebViewClient
gestisce la comunicazione con
il codice JavaScript inserito in WebView
.
Configura WebView e chiama Credential Manager:
val credentialManagerHandler = CredentialManagerHandler(this)
setContent {
val coroutineScope = rememberCoroutineScope()
AndroidView(factory = {
WebView(it).apply {
settings.javaScriptEnabled = true
// Test URL:
val url = "https://passkeys-codelab.glitch.me/"
val listenerSupported = WebViewFeature.isFeatureSupported(
WebViewFeature.WEB_MESSAGE_LISTENER
)
if (listenerSupported) {
// Inject local JavaScript that calls Credential Manager.
hookWebAuthnWithListener(
this, this@WebViewMainActivity,
coroutineScope, credentialManagerHandler
)
} else {
// Fallback routine for unsupported API levels.
}
loadUrl(url)
}
}
)
}
Crea un nuovo oggetto client WebView e inserisci JavaScript nella pagina web:
val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler)
val webViewClient = object : WebViewClient() {
override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
super.onPageStarted(view, url, favicon)
webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
}
}
webView.webViewClient = webViewClient
Configurare un listener di messaggi web
Per consentire la pubblicazione di messaggi tra JavaScript e l'app per Android, configura un
listener di messaggi web con il metodo WebViewCompat.addWebMessageListener
.
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME,
rules, passkeyWebListener)
}
Integrazione web
Per scoprire come creare l'integrazione web, consulta Creare una passkey per accessi senza password e Accedere con una passkey tramite il completamento automatico dei moduli.
Test e deployment
Testa attentamente l'intero flusso in un ambiente controllato per garantire una corretta comunicazione tra l'app per Android, la pagina web e il backend.
Esegui il deployment della soluzione integrata in produzione, assicurandoti che il backend possa gestire le richieste di registrazione e autenticazione in entrata. Il codice di backend deve generare il JSON iniziale per i processi di registrazione (creazione) e autenticazione (recupero). Inoltre, deve gestire la convalida e la verifica delle risposte ricevute dalla pagina web.
Verifica che l'implementazione corrisponda ai consigli per l'esperienza utente.
Note importanti
- Utilizza il codice JavaScript fornito per gestire le operazioni
navigator.credentials.create()
enavigator.credentials.get()
. - La classe
PasskeyWebListener
è il ponte tra l'app Android e il codice JavaScript in WebView. Gestisce il passaggio dei messaggi, la comunicazione e l'esecuzione delle azioni richieste. - Adatta gli snippet di codice forniti alla struttura, alle convenzioni di denominazione e a eventuali requisiti specifici del tuo progetto.
- Rileva gli errori sul lato dell'app nativa e inviali di nuovo al lato JavaScript.
Se segui questa guida e integri l'API Credential Manager nella tua app per Android che utilizza WebView, puoi offrire ai tuoi utenti un'esperienza di accesso sicura e senza interruzioni con passkey, gestendo al contempo le loro credenziali in modo efficace.