In diesem Dokument wird beschrieben, wie Sie die Credential Manager API in eine Android-App einbinden, die WebView verwendet.
Übersicht
Bevor Sie sich mit dem Integrationsprozess befassen, sollten Sie den Kommunikationsablauf zwischen nativem Android-Code, einer Webkomponente, die in einer WebView gerendert wird, die die Authentifizierung Ihrer App verwaltet, und einem Back-End verstehen. Der Ablauf umfasst die Registrierung (Erstellen von Anmeldedaten) und die Authentifizierung (Abrufen vorhandener Anmeldedaten).
Registrierung (Passkey erstellen)
- Das Back-End generiert eine JSON-Datei für die anfängliche Registrierung und sendet diese an die in WebView gerenderte Webseite.
- Die Webseite verwendet
navigator.credentials.create()
, um neue Anmeldedaten zu registrieren. Sie verwenden den eingeschleusten JavaScript-Code, um diese Methode in einem späteren Schritt zu überschreiben, um die Anfrage an die Android-App zu senden. - Die Android-App verwendet die Credential Manager API, um die Anfrage für die Anmeldedaten zu erstellen und für
createCredential
zu verwenden. - Die Credential Manager API teilt die Anmeldedaten des öffentlichen Schlüssels mit der App.
- Die Anwendung sendet die Anmeldedaten für den öffentlichen Schlüssel an die Webseite zurück, damit das injizierte JavaScript die Antworten parsen kann.
- Die Webseite sendet den öffentlichen Schlüssel an das Back-End, das den öffentlichen Schlüssel überprüft und speichert.
Authentifizierung (Passkey abrufen)
- Das Back-End generiert Authentifizierungs-JSON, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
- Die Webseite verwendet
navigator.credentials.get
. Überschreiben Sie diese Methode mit dem eingefügten JavaScript-Code, um die Anfrage an die Android-App weiterzuleiten. - Die Anwendung ruft die Anmeldedaten mithilfe der Credential Manager API ab, indem sie
getCredential
aufruft. - 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.
- Anschließend sendet die Webseite sie an den Server, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.
Derselbe Ablauf könnte für Passwörter oder föderierte Identitätssysteme verwendet werden.
Voraussetzungen
Führen Sie die Schritte im Abschnitt Voraussetzungen des Leitfadens zum Anmeldedatenmanager aus und gehen Sie so vor, um die Credential Manager API zu verwenden:
- 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 einem WebView und nativem 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 reduzierte Version dieses JavaScript-Codes in Ihrer Anwendung.
Listener für Passkeys erstellen
Richten Sie eine PasskeyWebListener
-Klasse ein, die die Kommunikation mit JavaScript verwaltet. Diese Klasse sollte von WebViewCompat.WebMessageListener
übernehmen. 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 innerhalb von PasskeyWebListener
die Logik für Anfragen und Antworten, wie in den folgenden Abschnitten beschrieben.
Authentifizierungsanfrage verarbeiten
Zum Verarbeiten 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
finden Sie im Beispiel auf GitHub.
Antwort verarbeiten
Um die Antworten zu verarbeiten, die von der nativen Anwendung an die Webseite gesendet werden, musst du JavaScriptReplyProxy
im JavaScriptReplyChannel
-Element hinzufügen.
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);
}
}
Achten Sie darauf, alle Fehler von der nativen App abzufangen und sie zurück an die JavaScript-Seite zu senden.
In WebView einbinden
In diesem Abschnitt wird beschrieben, wie Sie die WebView-Integration einrichten.
WebView initialisieren
Initialisiere in den Aktivitäten deiner Android-App ein WebView
und richte ein zugehöriges WebViewClient
ein. Der WebViewClient
übernimmt die Kommunikation mit dem in WebView
eingefügten JavaScript-Code.
WebView einrichten und Anmeldedaten-Manager aufrufen:
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
Damit Nachrichten zwischen JavaScript und der Android-App gepostet werden können, müssen Sie mit der Methode WebViewCompat.addWebMessageListener
einen Webnachrichten-Listener einrichten.
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 des Bezahlvorgangs mit Webintegration finden Sie unter Passkey für passwortlose Anmeldungen erstellen und Mit einem Passkey über Autofill für Formulare 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 Back-End sicherzustellen.
Stellen Sie die integrierte Lösung für die Produktion bereit und achten Sie darauf, dass das Back-End eingehende Registrierungs- und Authentifizierungsanfragen verarbeiten kann. Der Back-End-Code sollte die anfängliche JSON-Datei für Registrierungs- (Erstellungs-) und Authentifizierungs- (get)-Prozesse generieren. Er sollte auch die Validierung und Verifizierung der von der Webseite erhaltenen Antworten abdecken.
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
PasskeyWebListener
-Klasse ist das Bindeglied zwischen der Android-App und dem JavaScript-Code im WebView. Sie übernimmt die Nachrichtenweitergabe, die Kommunikation und die Ausführung der erforderlichen Aktionen. - Passen Sie die bereitgestellten Code-Snippets an die Struktur Ihres Projekts, die Namenskonventionen und alle spezifischen Anforderungen an, die Sie haben.
- Erfassen Sie Fehler in der nativen App und senden Sie sie zurück an die JavaScript-Seite.
Wenn du diesem Leitfaden folgst und die Credential Manager API in deine Android-App einbindest, in der WebView verwendet wird, kannst du deinen Nutzern eine sichere und nahtlose Anmeldung mit Passkeys ermöglichen und gleichzeitig ihre Anmeldedaten effektiv verwalten.