This document describes how to integrate the Credential Manager API with an
Android app that uses WebView.

## Overview

Before diving into the integration process, it's important to understand the
flow of communication between native Android code, a web component rendered
within a WebView that manages your app's authentication, and a backend. The flow
involves [*registration*](https://developer.android.com/identity/sign-in/credential-manager-webview#registration) (creating credentials) and [*authentication*](https://developer.android.com/identity/sign-in/credential-manager-webview#authentication)
(obtaining existing credentials).

### Registration (create a passkey)

1. The backend generates initial [registration JSON](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson) and sends it to the web page rendered within the WebView.
2. The web page uses [`navigator.credentials.create()`](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create) to register new credentials. You will use the injected JavaScript to override this method in a later step to send the request to the Android app.
3. The Android app uses the Credential Manager API to construct the credential [request](https://developer.android.com/reference/androidx/credentials/CreateCredentialRequest) and use it to [`createCredential`](https://developer.android.com/reference/androidx/credentials/CredentialManager#createCredential(android.content.Context,androidx.credentials.CreateCredentialRequest)).
4. The Credential Manager API shares the public key credential with the app.
5. The app sends the public key credential back to the web page so that the injected JavaScript can parse the responses.
6. The web page sends the public key to the backend, which verifies and saves the public key.

![Chart showing the passkey registration flow](https://developer.android.com/static/identity/sign-in/images/credman-webview1.png) **Figure 1.** The passkey registration flow.

### Authentication (get a passkey)

1. The backend generates [authentication JSON](https://w3c.github.io/webauthn/#sctn-parseRequestOptionsFromJSON) to get the credential and sends this to the web page that is rendered in the WebView client.
2. The web page uses [`navigator.credentials.get`](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get). Use the injected JavaScript to override this method to redirect the request to the Android app.
3. The app retrieves the credential using the Credential Manager API by calling [`getCredential`](https://developer.android.com/reference/androidx/credentials/CredentialManager#getCredential(android.content.Context,androidx.credentials.GetCredentialRequest)).
4. The Credential Manager API returns the credential to the app.
5. The app gets the digital signature of the private key and sends it to the web page so that the injected JavaScript can parse the responses.
6. Then the web page sends it to the server that verifies the digital signature with the public key.

![Chart showing the passkey authentication flow](https://developer.android.com/static/identity/sign-in/images/credman-webview2.png) **Figure 2.** The passkey authentication flow.

The same flow could be used for passwords or federated identity systems.

## Prerequisites

To use Credential Manager API, complete the steps outlined in the
[prerequisites](https://developer.android.com/training/sign-in/passkeys#prerequisites) section of the Credential Manager guide, and make sure you
do the following:

- [Add required dependencies](https://developer.android.com/training/sign-in/passkeys#add-dependencies).
- [Preserve classes in the ProGuard file](https://developer.android.com/training/sign-in/passkeys#proguard).
- [Add support for Digital Asset Links](https://developer.android.com/training/sign-in/passkeys#add-support-dal).

## JavaScript communication

To allow JavaScript in a WebView and native Android code to talk to each other,
you need to send messages and handle requests between the two environments. To
do this, inject custom JavaScript code into a WebView. This lets you modify the
behavior of web content and interact with native Android code.

### JavaScript injection

The following [JavaScript code](https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/javascript/encode.js) establishes communication
between the WebView and the Android app. It overrides
[`navigator.credentials.create()`](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-create) and
[`navigator.credentials.get()`](https://w3c.github.io/webappsec-credential-management/#dom-credentialscontainer-get) methods that are used by the
[WebAuthn API](https://w3c.github.io/webauthn/) for registration and authentication flows
described earlier.

Use [the minified version of this JavaScript code](https://github.com/android/identity-samples/blob/main/CredentialManagerWebView/PasskeyWebListener.kt#L234) in your
application.

### Create a listener for passkeys

Set up a [`PasskeyWebListener` class](https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/PasskeyWebListener.kt) that handles communication
with JavaScript. This class should inherit from
[`WebViewCompat.WebMessageListener`](https://developer.android.com/reference/androidx/webkit/WebViewCompat.WebMessageListener). This class receives messages from
JavaScript and performs the necessary actions in the Android app.

The following sections describe the structure of the `PasskeyWebListener` class,
as well as the handling of requests and responses.

### Handle the authentication request

To handle requests for WebAuthn `navigator.credentials.create()` or
`navigator.credentials.get()` operations, the `onPostMessage` method of the
`PasskeyWebListener` class is called when the JavaScript code sends a message to
the Android app:  

    // The class talking to Javascript should inherit:
    class PasskeyWebListener(
        private val activity: Activity,
        private val coroutineScope: CoroutineScope,
        private val credentialManagerHandler: CredentialManagerHandler
    ) : WebViewCompat.WebMessageListener {
        /** 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 {
            /** INTERFACE_NAME is the name of the MessagePort that must be injected into pages. */
            const val INTERFACE_NAME = "__webauthn_interface__"
            const val TYPE_KEY = "type"
            const val REQUEST_KEY = "request"
            const val CREATE_UNIQUE_KEY = "create"
            const val GET_UNIQUE_KEY = "get"
            /** INJECTED_VAL is the minified version of the JavaScript code described at this class
             * heading. The non minified form is found at credmanweb/javascript/encode.js.*/
            const val INJECTED_VAL = """
                var __webauthn_interface__,__webauthn_hooks__;!function(e){console.log("In the hook."),__webauthn_interface__.addEventListener("message",function e(n){var r=JSON.parse(n.data),t=r[2];"get"===t?o(r):"create"===t?u(r):console.log("Incorrect response format for reply")});var n=null,r=null,t=null,a=null;function o(e){if(null!==n&&null!==t){if("success"!=e[0]){var r=t;n=null,t=null,r(new DOMException(e[1],"NotAllowedError"));return}var a=i(e[1]),o=n;n=null,t=null,o(a)}}function l(e){var n=e.length%4;return Uint8Array.from(atob(e.replace(/-/g,"+").replace(/_/g,"/").padEnd(e.length+(0===n?0:4-n),"=")),function(e){return e.charCodeAt(0)}).buffer}function s(e){return btoa(Array.from(new Uint8Array(e),function(e){return String.fromCharCode(e)}).join("")).replace(/\+/g,"-").replace(/\//g,"_").replace(/=+${'$'}/,"")}function u(e){if(null===r||null===a){console.log("Here: "+r+" and reject: "+a);return}if(console.log("Output back: "+e),"success"!=e[0]){var n=a;r=null,a=null,n(new DOMException(e[1],"NotAllowedError"));return}var t=i(e[1]),o=r;r=null,a=null,o(t)}function i(e){return console.log("Here is the response from credential manager: "+e),e.rawId=l(e.rawId),e.response.clientDataJSON=l(e.response.clientDataJSON),e.response.hasOwnProperty("attestationObject")&&(e.response.attestationObject=l(e.response.attestationObject)),e.response.hasOwnProperty("authenticatorData")&&(e.response.authenticatorData=l(e.response.authenticatorData)),e.response.hasOwnProperty("signature")&&(e.response.signature=l(e.response.signature)),e.response.hasOwnProperty("userHandle")&&(e.response.userHandle=l(e.response.userHandle)),e.getClientExtensionResults=function e(){return{}},e}e.create=function n(t){if(!("publicKey"in t))return e.originalCreateFunction(t);var o=new Promise(function(e,n){r=e,a=n}),l=t.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}if(l.hasOwnProperty("user")&&l.user.hasOwnProperty("id")){var i=s(l.user.id);l.user.id=i}var c=JSON.stringify({type:"create",request:l});return __webauthn_interface__.postMessage(c),o},e.get=function r(a){if(!("publicKey"in a))return e.originalGetFunction(a);var o=new Promise(function(e,r){n=e,t=r}),l=a.publicKey;if(l.hasOwnProperty("challenge")){var u=s(l.challenge);l.challenge=u}var i=JSON.stringify({type:"get",request:l});return __webauthn_interface__.postMessage(i),o},e.onReplyGet=o,e.CM_base64url_decode=l,e.CM_base64url_encode=s,e.onReplyCreate=u}(__webauthn_hooks__||(__webauthn_hooks__={})),__webauthn_hooks__.originalGetFunction=navigator.credentials.get,__webauthn_hooks__.originalCreateFunction=navigator.credentials.create,navigator.credentials.get=__webauthn_hooks__.get,navigator.credentials.create=__webauthn_hooks__.create,window.PublicKeyCredential=function(){},window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable=function(){return Promise.resolve(!1)};
            """
        }

For `handleCreateFlow` and `handleGetFlow`, refer to [the example on
GitHub](https://github.com/android/identity-samples/blob/main/WebView/CredentialManagerWebView/PasskeyWebListener.kt).

### Handle the response

To handle the responses that are sent from the native app to the web page, add
the `JavaScriptReplyProxy` within the `JavaScriptReplyChannel`.  

    // 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?)
    }

Be sure to catch any errors from the native app and send them back to the
JavaScript side.

## Integrate with WebView

This section describes how to set up your WebView integration.

### Initialize the WebView

In your Android app's activity, initialize a `WebView` and set up an
accompanying `WebViewClient`. The `WebViewClient` handles the communication with
the JavaScript code injected into `WebView`.

Set up the WebView and call Credential Manager:  

    val credentialManagerHandler = CredentialManagerHandler(this)

    setContent {
        val coroutineScope = rememberCoroutineScope()
        AndroidView(
            factory = {
                WebView(it).apply {
                    settings.javaScriptEnabled = true

                    // Test URL:
                    val url = "https://passkeys-codelab.glitch.me/"
                    val listenerSupported = WebViewFeature.isFeatureSupported(
                        WebViewFeature.WEB_MESSAGE_LISTENER
                    )
                    if (listenerSupported) {
                        // Inject local JavaScript that calls Credential Manager.
                        hookWebAuthnWithListener(
                            this, this@WebViewMainActivity,
                            coroutineScope, credentialManagerHandler
                        )
                    } else {
                        // Fallback routine for unsupported API levels.
                    }
                    loadUrl(url)
                }
            }
        )
    }

Create a new WebView client object and inject JavaScript into the web page:  

    val passkeyWebListener = PasskeyWebListener(activity, coroutineScope, credentialManagerHandler)

    val webViewClient = object : WebViewClient() {
        override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
            super.onPageStarted(view, url, favicon)
            webView.evaluateJavascript(PasskeyWebListener.INJECTED_VAL, null)
        }
    }

    webView.webViewClient = webViewClient

### Set up a web message listener

To allow messages to be posted between JavaScript and the Android app, set up a
web message listener with the `WebViewCompat.addWebMessageListener` method.  

    val rules = setOf("*")
    if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
        WebViewCompat.addWebMessageListener(
            webView, PasskeyWebListener.INTERFACE_NAME,
            rules, passkeyWebListener
        )
    }

## Web integration

To learn how to build Web integration checkout [Create a passkey for
passwordless logins](https://web.dev/passkey-registration/) and [Sign in with a passkey through form
autofill](https://web.dev/passkey-form-autofill/).

## Testing and deployment

Test the entire flow thoroughly in a controlled environment to ensure proper
communication between the Android app, the web page, and the backend.

Deploy the integrated solution to production, ensuring that the backend can
handle incoming registration and authentication requests. The backend code
should generate initial JSON for registration (create) and authentication (get)
processes. It should also handle validation and verification of the responses
received from the web page.

Verify the implementation corresponds to the [UX recommendations](https://developer.android.com/design/ui/mobile/guides/patterns/passkeys).

## Important notes

- Use the provided JavaScript code to handle `navigator.credentials.create()` and `navigator.credentials.get()` operations.
- The `PasskeyWebListener` class is the bridge between the Android app and the JavaScript code in the WebView. It handles message passing, communication, and execution of required actions.
- Adapt the provided code snippets to fit your project's structure, naming conventions, and any specific requirements you might have.
- Catch errors on the native app side and send them back to the JavaScript side.

By following this guide and integrating the Credential Manager API into your
Android app that uses WebView, you can provide a secure and seamless
passkey-enabled login experience for your users while managing their credentials
effectively.