Request digital credentials in WebView

This guide describes how to request digital credentials using Credential Manager in an Android app that uses WebView. Use a JavaScript Bridge to facilitate communication between the web page and the app.

Overview

The following steps describe the life of a digital credential request from a WebView:

  1. Request generation: The web page in the WebView makes a request to the app server, which generates an OpenID4VP request JSON and sends it back.
  2. Request initiation: The web page uses navigator.credentials.get({ digital: ... }) to request digital credentials using the request JSON.
  3. Interception: The injected bridge.js intercepts this call, extracts the specific OpenID4VP request JSON, and passes it to the Android app.
  4. Android Handling: The Android app receives the message and calls the CredentialManager.getCredential API.
  5. User selection: Credential Manager displays a UI prompting the user to share their credential. The credential is returned to the Android app.
  6. Bridge response: The Android app sends the response back to the web page using the bridge.
  7. Completion: The web page receives and handles the response.

Prerequisites

Before you implement the JavaScript bridge for digital credentials in WebView, ensure that your project meets the following requirements:

  • Android version compatibility: Your app must target Android 6.0 (API level 23) or higher.
  • Host app control: You must control the Android app hosting the WebView so that you can attach a WebMessageListener before loading the web page.
  • HTTPS and top-level frame: The web page requesting the credential must be served over https:// and run in the main frame (isMainFrame must be true). Subframes and embedded <iframe> elements aren't supported.
  • Experimental API opt-in: Because the Digital Credentials API is experimental, you must annotate classes or methods that use GetDigitalCredentialOption with @OptIn(ExperimentalDigitalCredentialApi::class).

Add dependencies

Add the Credential Manager and AndroidX WebKit dependencies to your app module's build.gradle.kts file:

dependencies {
    // Credential Manager dependencies
    implementation("androidx.credentials:credentials:1.7.0-alpha03")
    implementation("androidx.credentials:credentials-play-services-auth:1.7.0-alpha03")

    // AndroidX WebKit (required for WebMessageListener bridge)
    implementation("androidx.webkit:webkit:1.14.0")
}

Implement digital credentials in the WebView

You'll need to add JavaScript to your WebView to make the credential request and intercept it with bridge.js.

Create the JavaScript request

To communicate between JavaScript running in a WebView and Android code, inject JavaScript code into your WebView:

// Example digital credential request to request verified email
const request = {
  "providers": [
      {
        "protocol": "openid4vp",
        "request": JSON.stringify({
          "requests": [
            {
              "protocol": "openid4vp-v1-unsigned",
              "data": {
                "response_type": "vp_token",
                "response_mode": "dc_api",
                "nonce": "n-0S6_W_2F", // Generate dynamically
                "dcql_query": {
                  "credentials": [
                    {
                      "id": "user_info_query",
                      "format": "dc+sd-jwt",
                      "meta": {
                        "vct_values": ["UserInfoCredential"]
                      },
                      "claims": [
                        {"path": ["email"]},
                        {"path": ["email_verified"]},
                        {"path": ["name"]}
                      ]
                    }
                  ]
                }
              }
            }
          ]
      })
    }
  ]
};

try {
  const credential = await navigator.credentials.get({
    digital: request
  });

  // Process response
} catch (err) {
  // Handle error
}

Create the JavaScript bridge

Create a file named bridge.js or inject this code using evaluateJavascript. This script overrides navigator.credentials.get to intercept the request and send the inner OpenID4VP JSON string to the Android app.

(function() {
  if (!navigator.credentials) {
    navigator.credentials = {};
  }

  const originalGet = navigator.credentials.get;

  // Overrides navigator.credentials.get() to intercept W3C Digital Credential requests.
  navigator.credentials.get = async function(options) {

    if (options && options.digital) {
      if (window.DigitalCredentialBridge) {
        return new Promise((resolve, reject) => {
          const digital = options.digital;

          if (digital.providers && digital.providers.length > 0) {
            const provider = digital.providers[0];

            if (provider.protocol === "openid4vp") {
              const requestJson = provider.request;

              const message = {
                type: "get",
                request: requestJson
              };

              console.log("Sending message to native bridge:", JSON.stringify(message));
              // Send the request string to native Android code
              window.DigitalCredentialBridge.postMessage(JSON.stringify(message));

              // Receive the response sent from native code using replyProxy.postMessage()
              window.DigitalCredentialBridge.onmessage = function(event) {
                console.log("Received response from native bridge:", event.data);
                try {
                  const response = JSON.parse(event.data);
                  if (response.error) {
                    reject(new Error(response.error));
                  } else {
                    resolve(response);
                  }
                } catch (e) {
                  // If it's not JSON, just resolve with the raw data
                  resolve(event.data);
                }
              };
            } else {
              reject(new Error("Unsupported protocol: " + provider.protocol));
            }
          } else {
            reject(new Error("No providers specified in digital credential request"));
          }
        });
      } else {
        console.warn("DigitalCredentialBridge not found, falling back to original");
      }
    }

    if (originalGet) {
      return originalGet.call(navigator.credentials, options);
    } else {
      throw new Error("navigator.credentials.get is not supported and no bridge available");
    }
  };
})();

Implement digital credentials in Android

Configure your Android app to handle credential requests sent from bridge.js.

Create the Android listener

Set up a DigitalCredentialWebListener class that implements WebViewCompat.WebMessageListener. This class handles bidirectional messages between JavaScript and native Android code, enforcing security checks on isMainFrame and sourceOrigin and preventing overlapping requests.

Implement the Credential Manager Handler

Call getCredential to request digital credentials from the device.

@OptIn(ExperimentalDigitalCredentialApi::class)
class CredentialManagerHandler(private val context: Context) {
  private val credentialManager = CredentialManager.create(context)

  suspend fun getDigitalCredential(requestJson: String): String {
    val digitalCredentialOption = GetDigitalCredentialOption(requestJson)
    val getCredRequest = GetCredentialRequest(listOf(digitalCredentialOption))

    return try {
      val result = credentialManager.getCredential(context, getCredRequest)
      val credential = result.credential
      if (credential is DigitalCredential) {
        credential.credentialJson
      } else {
        "{\"error\": \"Unexpected credential type\"}"
      }
    } catch (e: GetCredentialException) {
      "{\"error\": \"${e.message}\"}"
    } catch (e: Exception) {
      "{\"error\": \"Unknown exception\"}"
    }
  }

  companion object {
    private const val TAG = "CredManHandler"
  }
}

Apply the framework to your Activity

In your Activity or View, register the WebMessageListener and associated functions.

// Inside your Activity or View setup
AndroidView(
  factory = { context ->
    val handler = CredentialManagerHandler(context)
    WebView(context).apply {
      settings.javaScriptEnabled = true

      // Register the bridge before loading URL
      if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
                val listener = DigitalCredentialWebListener(coroutineScope, credentialManagerHandler)
                WebViewCompat.addWebMessageListener(
                    webView,
                    INTERFACE_NAME,
                    setOf("*"),
                    listener
                )


      loadUrl("https://your-secure-domain.com/login")
    }
  },
)

Testing the flow

Test the flow to confirm communication between the WebView and the Android app. The credential selector appears in the WebView during the process, and the selected credential returns to the WebView upon completion.