In diesem Dokument wird beschrieben, wie Sie die Credential Manager API in eine Android-App einbinden, die WebView verwendet.
Übersicht
Bevor Sie mit der Integration beginnen, ist es wichtig, den Kommunikationsfluss zwischen nativem Android-Code, einer Webkomponente, die in einer WebView gerendert wird und die Authentifizierung Ihrer App verwaltet, und einem Backend zu verstehen. Der Ablauf umfasst die Registrierung (Anmeldedaten erstellen) und die Authentifizierung (vorhandene Anmeldedaten abrufen).
Registrierung (Passkey erstellen)
- Das Backend generiert das erste Registrierungs-JSON und sendet es an die Webseite, die im WebView gerendert wird.
- Auf der Webseite wird
navigator.credentials.create()
verwendet, um neue Anmeldedaten zu registrieren. Sie verwenden das eingefügte JavaScript, um diese Methode in einem späteren Schritt zu überschreiben und die Anfrage an die Android-App zu senden. - Die Android-App verwendet die Credential Manager API, um die Anmeldedatenanfrage zu erstellen und damit
createCredential
. - Die Credential Manager API gibt die Anmeldedaten für den öffentlichen Schlüssel an die App weiter.
- Die App sendet die Anmeldedaten mit dem öffentlichen Schlüssel zurück an die Webseite, damit das eingefügte JavaScript die Antworten parsen kann.
- Die Webseite sendet den öffentlichen Schlüssel an das Backend, das ihn verifiziert und speichert.

Authentifizierung (Passkey abrufen)
- Das Backend generiert Authentifizierungs-JSON, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
- Auf der Webseite wird
navigator.credentials.get
verwendet. Verwenden Sie das eingefügte JavaScript, um diese Methode zu überschreiben und die Anfrage an die Android-App weiterzuleiten. - Die App ruft die Anmeldedaten mit der Credential Manager API durch Aufrufen von
getCredential
ab. - Die Credential Manager API gibt die Anmeldedaten an die App zurück.
- Die App ruft die digitale Signatur des privaten Schlüssels ab und sendet sie an die Webseite, damit das eingefügte JavaScript die Antworten parsen kann.
- Anschließend wird sie an den Server gesendet, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.

Derselbe Ablauf kann für Passwörter oder föderierte Identitätssysteme verwendet werden.
Voraussetzungen
Wenn Sie die Credential Manager API verwenden möchten, führen Sie die Schritte im Abschnitt Voraussetzungen des Credential Manager-Leitfadens aus und achten Sie darauf, dass Sie Folgendes tun:
- Erforderliche Abhängigkeiten hinzufügen
- Klassen in der ProGuard-Datei beibehalten:
- Unterstützung für Digital Asset Links hinzufügen
JavaScript-Kommunikation
Damit JavaScript in einem WebView und nativer Android-Code miteinander kommunizieren können, müssen Sie Nachrichten zwischen den beiden Umgebungen senden und Anfragen verarbeiten. Dazu fügen Sie benutzerdefinierten JavaScript-Code in eine WebView ein. So können Sie das Verhalten von Webinhalten ändern und mit nativem Android-Code interagieren.
JavaScript-Einschleusung
Der folgende JavaScript-Code stellt die Kommunikation zwischen der WebView und der Android-App her. Er überschreibt die Methoden navigator.credentials.create()
und navigator.credentials.get()
, die von der WebAuthn API für die oben beschriebenen Registrierungs- und Authentifizierungsabläufe verwendet werden.
Verwenden Sie die minimierte Version dieses JavaScript-Codes in Ihrer Anwendung.
Listener für Passkeys erstellen
Richten Sie eine PasskeyWebListener
-Klasse ein, die die Kommunikation mit JavaScript übernimmt. Diese Klasse sollte von WebViewCompat.WebMessageListener
abgeleitet werden. Diese Klasse empfängt Nachrichten von JavaScript und führt die erforderlichen Aktionen in der Android-App aus.
In den folgenden Abschnitten wird die Struktur der PasskeyWebListener
-Klasse sowie die Verarbeitung von Anfragen und Antworten beschrieben.
Authentifizierungsanfrage verarbeiten
Wenn Anfragen für WebAuthn-Vorgänge vom Typ navigator.credentials.create()
oder navigator.credentials.get()
verarbeitet werden sollen, wird die Methode onPostMessage
der Klasse PasskeyWebListener
aufgerufen, wenn der JavaScript-Code eine Nachricht an die Android-App sendet:
// 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)};
"""
}
Informationen zu handleCreateFlow
und handleGetFlow
finden Sie im Beispiel auf GitHub.
Antwort verarbeiten
Um die Antworten zu verarbeiten, die von der nativen App an die Webseite gesendet werden, fügen Sie JavaScriptReplyProxy
in JavaScriptReplyChannel
ein.
// 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?)
}
Achten Sie darauf, alle Fehler aus der nativen App abzufangen und an die JavaScript-Seite zurückzugeben.
In WebView einbinden
In diesem Abschnitt wird beschrieben, wie Sie Ihre WebView-Integration einrichten.
WebView initialisieren
Initialisieren Sie in der Aktivität Ihrer Android-App ein WebView
und richten Sie ein zugehöriges WebViewClient
ein. WebViewClient
übernimmt die Kommunikation mit dem JavaScript-Code, der in WebView
eingefügt wird.
WebView einrichten und Credential Manager aufrufen:
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)
}
}
)
}
Erstellen Sie ein neues WebView-Clientobjekt und fügen Sie JavaScript in die Webseite ein:
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
Webnachrichten-Listener einrichten
Damit Nachrichten zwischen JavaScript und der Android-App gesendet werden können, müssen Sie mit der Methode WebViewCompat.addWebMessageListener
einen Web Message Listener einrichten.
val rules = setOf("*")
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
WebViewCompat.addWebMessageListener(webView, PasskeyWebListener.INTERFACE_NAME,
rules, passkeyWebListener)
}
Web integration
Informationen zum Erstellen einer Web-Integration für die Kaufabwicklung finden Sie unter Passkey für die passwortlose Anmeldung erstellen und Mit einem Passkey über das automatische Ausfüllen von Formularen anmelden.
Testen und Bereitstellen
Testen Sie den gesamten Ablauf gründlich in einer kontrollierten Umgebung, um eine ordnungsgemäße Kommunikation zwischen der Android-App, der Webseite und dem Backend sicherzustellen.
Stellen Sie die integrierte Lösung in der Produktion bereit und sorgen Sie dafür, dass das Backend eingehende Registrierungs- und Authentifizierungsanfragen verarbeiten kann. Der Backend-Code sollte das erste JSON für die Registrierung (create) und die Authentifizierung (get) generieren. Außerdem sollten die von der Webseite empfangenen Antworten validiert und bestätigt werden.
Prüfen Sie, ob die Implementierung den UX-Empfehlungen entspricht.
Wichtige Hinweise
- Verwenden Sie den bereitgestellten JavaScript-Code, um
navigator.credentials.create()
- undnavigator.credentials.get()
-Vorgänge zu verarbeiten. - Die Klasse
PasskeyWebListener
ist die Brücke zwischen der Android-App und dem JavaScript-Code in der WebView. Sie kümmert sich um die Nachrichtenübermittlung, die Kommunikation und die Ausführung der erforderlichen Aktionen. - Passen Sie die bereitgestellten Code-Snippets an die Struktur, die Namenskonventionen und alle spezifischen Anforderungen Ihres Projekts an.
- Fehler auf der Seite der nativen App abfangen und an die JavaScript-Seite zurücksenden.
Wenn Sie dieser Anleitung folgen und die Credential Manager API in Ihre Android-App mit WebView einbinden, können Sie Ihren Nutzern eine sichere und nahtlose Anmeldung mit Passkeys ermöglichen und gleichzeitig ihre Anmeldedaten effektiv verwalten.