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 einem WebView gerendert wird und die Authentifizierung Ihrer App verwaltet, und einem Backend zu verstehen. Der Ablauf umfasst die Registrierung (Erstellen von Anmeldedaten) und die Authentifizierung (Abrufen vorhandener Anmeldedaten).
Registrierung (Passkey erstellen)
- Das Backend generiert die ersten JSON-Daten für die Registrierung und sendet sie an die Webseite, die in der WebView gerendert wird.
- Die Webseite verwendet
navigator.credentials.create()
, um neue Anmeldedaten zu registrieren. Mit dem eingeschleusten JavaScript überschreiben Sie diese Methode in einem späteren Schritt, um die Anfrage an die Android-App zu senden. - Die Android-App verwendet die Credential Manager API, um die Anfrage für Anmeldedaten zu erstellen und für
createCredential
zu verwenden. - Die Credential Manager API gibt die Public-Key-Anmeldedaten an die App weiter.
- Die App sendet die Anmeldedaten für den öffentlichen Schlüssel zurück an die Webseite, damit das eingeschleuste JavaScript die Antworten analysieren kann.
- Die Webseite sendet den öffentlichen Schlüssel an das Backend, das ihn überprüft und speichert.

Authentifizierung (Passkey abrufen)
- Das Backend generiert JSON-Authentifizierungsdaten, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
- Auf der Webseite wird
navigator.credentials.get
verwendet. Überschreiben Sie diese Methode mit dem eingeschleusten JavaScript, um die Anfrage an die Android-App weiterzuleiten. - Die Anwendung ruft die Anmeldedaten über die Credential Manager API ab, indem sie
getCredential
aufruft. - 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 eingeschleuste JavaScript die Antworten analysieren kann.
- Die Webseite sendet sie dann an den Server, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.

Derselbe Ablauf kann für Passwörter oder Systeme für föderierte Identitäten verwendet werden.
Voraussetzungen
Wenn Sie die Credential Manager API verwenden möchten, müssen Sie die Schritte im Abschnitt Voraussetzungen des Credential Manager-Leitfadens ausführen. Außerdem müssen folgende Voraussetzungen erfüllt sein:
- Fügen Sie die erforderlichen Abhängigkeiten hinzu.
- Klassen in der ProGuard-Datei beibehalten.
- Unterstützung für Digital Asset Links hinzufügen
JavaScript-Kommunikation
Damit JavaScript in einer WebView und nativer Android-Code miteinander kommunizieren können, musst du Nachrichten senden und Anfragen zwischen den beiden Umgebungen verarbeiten. Dazu müssen Sie benutzerdefinierten JavaScript-Code in eine WebView einschleusen. So können Sie das Verhalten von Webinhalten ändern und mit nativem Android-Code interagieren.
JavaScript-Injection
Der folgende JavaScript-Code stellt die Kommunikation zwischen der WebView und der Android-App her. Dabei werden die Methoden navigator.credentials.create()
und navigator.credentials.get()
überschrieben, die von der WebAuthn API für die oben beschriebenen Registrierungs- und Authentifizierungsabläufe verwendet werden.
Verwenden Sie in Ihrer Anwendung die minimierte Version dieses JavaScript-Codes.
Listener für Passkeys erstellen
Richte eine PasskeyWebListener
-Klasse ein, die die Kommunikation mit JavaScript verarbeitet. Diese Klasse sollte von WebViewCompat.WebMessageListener
abgeleitet sein. Diese Klasse empfängt Nachrichten von JavaScript und führt die erforderlichen Aktionen in der Android-App aus.
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
}
Implementieren Sie in PasskeyWebListener
die Logik für Anfragen und Antworten, wie in den folgenden Abschnitten beschrieben.
Authentifizierungsanfrage verarbeiten
Um Anfragen für WebAuthn-navigator.credentials.create()
- oder navigator.credentials.get()
-Vorgänge zu verarbeiten, wird die Methode onPostMessage
der Klasse PasskeyWebListener
aufgerufen, wenn der JavaScript-Code eine Nachricht an die Android-App sendet:
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();
}
}
}
}
Informationen zu handleCreateFlow
und handleGetFlow
findest du in diesem Beispiel auf GitHub.
Antwort verarbeiten
Wenn du die Antworten verarbeiten möchtest, die von der nativen App an die Webseite gesendet werden, füge die JavaScriptReplyProxy
in die JavaScriptReplyChannel
ein.
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);
}
}
Fangen Sie alle Fehler in der nativen App ab und senden Sie sie an die JavaScript-Seite zurück.
In WebView einbinden
In diesem Abschnitt wird beschrieben, wie Sie die WebView-Integration einrichten.
WebView initialisieren
Initialisieren Sie in den Aktivitäten Ihrer Android-App eine WebView
und richten Sie eine zugehörige WebViewClient
ein. Der WebViewClient
verwaltet die Kommunikation mit dem JavaScript-Code, der in WebView
eingefügt wird.
Richten Sie die WebView ein und rufen Sie den Anmeldedaten-Manager auf:
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);
}
Erstellen Sie ein neues WebView-Clientobjekt und fügen Sie JavaScript in die Webseite ein:
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);
Webnachrichten-Listener einrichten
Wenn Nachrichten zwischen JavaScript und der Android-App gepostet werden sollen, richten Sie einen Webnachrichten-Listener mit der Methode WebViewCompat.addWebMessageListener
ein.
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
)
}
Web integration
Unter den folgenden Links erfahren Sie, wie Sie einen Bezahlvorgang mit Webintegration erstellen: Passkey für passwortlose Anmeldungen erstellen und Mit einem Passkey über das Autofill für Formulare anmelden.
Tests und Bereitstellung
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.
Implementieren Sie die integrierte Lösung in der Produktion und achten Sie darauf, dass das Backend eingehende Registrierungs- und Authentifizierungsanfragen verarbeiten kann. Der Backend-Code sollte die anfängliche JSON-Datei für die Registrierung (Erstellen) und Authentifizierung (Abrufen) generieren. Außerdem sollte es die Validierung und Überprüfung der von der Webseite empfangenen Antworten übernehmen.
Prüfen Sie, ob die Implementierung den UX-Empfehlungen entspricht.
Wichtige Hinweise
- Verwende den bereitgestellten JavaScript-Code, um
navigator.credentials.create()
- undnavigator.credentials.get()
-Vorgänge zu verarbeiten. - Die
PasskeyWebListener
-Klasse ist die Brücke zwischen der Android-App und dem JavaScript-Code in der WebView. Er übernimmt die Nachrichtenweitergabe, die Kommunikation und die Ausführung der erforderlichen Aktionen. - Passen Sie die bereitgestellten Code-Snippets an die Struktur, die Namenskonventionen und etwaige spezifische Anforderungen Ihres Projekts an.
- Fehler auf der Seite der nativen App erfassen und an die JavaScript-Seite zurücksenden.
Wenn Sie dieser Anleitung folgen und die Anmeldedaten-Manager-API in Ihre Android-App mit WebView einbinden, können Sie Ihren Nutzern eine sichere und nahtlose Anmeldung über Passkeys ermöglichen und gleichzeitig ihre Anmeldedaten effektiv verwalten.