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.
- Auf der Webseite werden neue Anmeldedaten mit
navigator.credentials.create()
registriert. 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
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 anfordern)
- Das Backend generiert JSON-Authentifizierungsdaten, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
- Die Webseite verwendet
navigator.credentials.get
. 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 ab, indem
getCredential
aufgerufen wird. - Die Credential Manager API gibt die Anmeldedaten an die Anwendung zurück.
- Die Anwendung ruft die digitale Signatur des privaten Schlüssels ab und sendet sie an die Webseite, damit das eingefügte JavaScript die Antworten parsen kann.
- Die Webseite sendet sie dann an den Server, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.
Der gleiche Ablauf kann auch 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 aus, die im Abschnitt Voraussetzungen des Leitfadens für den Anmeldedaten-Manager beschrieben sind. Beachten Sie dabei Folgendes:
- 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, müssen Sie 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
Zur Verarbeitung von Anfragen für navigator.credentials.create()
- oder navigator.credentials.get()
-Vorgänge von WebAuthn 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
Fügen Sie JavaScriptReplyProxy
in JavaScriptReplyChannel
ein, um die Antworten zu verarbeiten, die von der nativen Anwendung an die Webseite gesendet werden.
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
Weitere Informationen zum Erstellen einer Webintegration für den Bezahlvorgang finden Sie unter Passkey für passwortlose Anmeldungen erstellen und Über das automatische Ausfüllen von Formularen mit einem Passkey 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.
Implementieren Sie die integrierte Lösung in der Produktion und achten Sie darauf, dass das Backend eingehende Registrierungs- und Authentifizierungsanfragen verarbeiten kann. Der Back-End-Code sollte die erste JSON für Registrierungs- (Erstellung) und Authentifizierungsprozesse (get) 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 WebView. Er übernimmt die Nachrichtenweitergabe, die Kommunikation und die Ausführung erforderlicher Aktionen. - Passen Sie die bereitgestellten Code-Snippets an die Struktur, die Benennungskonventionen und alle spezifischen Anforderungen Ihres Projekts an.
- Sie können Fehler auf der Seite der nativen App erkennen 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.