Anmeldedaten-Manager in WebView einbinden

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)

  1. Das Back-End generiert eine JSON-Datei für die anfängliche Registrierung und sendet diese an die in WebView gerenderte Webseite.
  2. 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.
  3. Die Android-App verwendet die Credential Manager API, um die Anfrage für die Anmeldedaten zu erstellen und für createCredential zu verwenden.
  4. Die Credential Manager API teilt die Anmeldedaten des öffentlichen Schlüssels mit der App.
  5. Die Anwendung sendet die Anmeldedaten für den öffentlichen Schlüssel an die Webseite zurück, damit das injizierte JavaScript die Antworten parsen kann.
  6. Die Webseite sendet den öffentlichen Schlüssel an das Back-End, das den öffentlichen Schlüssel überprüft und speichert.
Diagramm zum Ablauf der Passkey-Registrierung
Abbildung 1. Ablauf zur Passkey-Registrierung

Authentifizierung (Passkey abrufen)

  1. Das Back-End generiert Authentifizierungs-JSON, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
  2. Die Webseite verwendet navigator.credentials.get. Überschreiben Sie diese Methode mit dem eingefügten JavaScript-Code, um die Anfrage an die Android-App weiterzuleiten.
  3. Die Anwendung ruft die Anmeldedaten mithilfe der Credential Manager API ab, indem sie getCredential aufruft.
  4. Die Credential Manager API gibt die Anmeldedaten an die Anwendung zurück.
  5. 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.
  6. Anschließend sendet die Webseite sie an den Server, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.
Diagramm, das den Passkey-Authentifizierungsvorgang zeigt
Abbildung 1. Passkey-Authentifizierungsablauf

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:

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()- und navigator.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.