Nutzer mit WebView authentifizieren

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)

  1. Das Backend generiert die ersten JSON-Daten für die Registrierung und sendet sie an die Webseite, die in der WebView gerendert wird.
  2. 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.
  3. Die Android-App verwendet die Credential Manager API, um die Anfrage für Anmeldedaten zu erstellen und createCredential zu verwenden.
  4. Die Credential Manager API gibt die Public-Key-Anmeldedaten an die App weiter.
  5. Die App sendet die Anmeldedaten für den öffentlichen Schlüssel zurück an die Webseite, damit das eingeschleuste JavaScript die Antworten analysieren kann.
  6. Die Webseite sendet den öffentlichen Schlüssel an das Backend, das ihn überprüft und speichert.
Diagramm zum Ablauf der Passkey-Registrierung
Abbildung 1: Der Ablauf der Passkey-Registrierung.

Authentifizierung (Passkey anfordern)

  1. Das Backend generiert JSON-Authentifizierungsdaten, um die Anmeldedaten abzurufen, und sendet diese an die Webseite, die im WebView-Client gerendert wird.
  2. 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.
  3. Die App ruft die Anmeldedaten mit der Credential Manager API ab, indem getCredential aufgerufen wird.
  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. Die Webseite sendet sie dann an den Server, der die digitale Signatur mit dem öffentlichen Schlüssel überprüft.
Diagramm zum Ablauf der Passkey-Authentifizierung
Abbildung 2: Der Ablauf der Passkey-Authentifizierung.

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:

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