Skip to content

bug: Android: onReceivedClientCertRequest unconditionally opens the system certificate picker — breaks payment flows on Samsung devices #570

@madamowicz-lbpro

Description

@madamowicz-lbpro

Bug Report

Capacitor Version

Latest Dependencies:
  @capacitor/cli: 8.4.0
  @capacitor/core: 8.4.0
  @capacitor/android: 8.4.0
  @capacitor/ios: 8.4.0
Installed Dependencies:
  @capacitor/core: 8.1.0
  @capacitor/android: 8.1.0
  @capacitor/cli: 8.1.0
  @capacitor/ios: 8.1.0

Plugin Version

@capgo/inappbrowser: 8.1.24

context(s)

ManualModel: false
AutoMode: false
CapgoCloud: false
OnPremise: false

Platform(s)

Android only. (iOS is unaffected — the didReceive challenge handler in WKWebViewController.swift falls through to performDefaultHandling for client certificate challenges, so no UI is shown.)

Current Behavior

WebViewDialog.java implements onReceivedClientCertRequest and unconditionally forwards every TLS client certificate request to the OS picker via KeyChain.choosePrivateKeyAlias(activity, ..., null /* alias */). There is no option gating this behavior (Options.java contains no related flag, checked up to 8.6.14).

As a result, any HTTPS server that sends an optional CertificateRequest during the TLS handshake — common with payment gateways (in our case PolCard / Fiserv Poland, ssl.dotpay.pl:443) — makes the Android system dialog "Choose certificate — The app X has requested a certificate..." pop up in the middle of a payment flow.

The impact is device-dependent, which makes it hard to diagnose:

On most devices the user credential store is empty, so users see nothing or a near-empty picker.
On Samsung (One UI) devices the keystore ships pre-populated with system entries (FindMyMobile, AttestationKey_com_wssyncmldm with CN=Fake), so every Samsung user gets a scary certificate dialog listing fake-looking certificates during checkout. Non-technical users abandon the payment at this point.
Selecting "Deny" lets the handshake complete normally (the server's request is optional), which confirms the prompt adds no value here — the connection works without a client certificate.

This is a regression in user-facing behavior vs v7.x, which did not override onReceivedClientCertRequest, so the WebViewClient default applied: the request was silently canceled and the handshake completed without a client certificate.

Expected Behavior

By default, a client certificate request should be canceled silently (pre-8.x behavior and the Android WebViewClient default). Showing the system picker should be opt-in, e.g.:

InAppBrowser.openWebView({
  url,
  clientCertificate: 'prompt' // default: 'none' (cancel silently)
});

Alternatively, only invoke KeyChain.choosePrivateKeyAlias when the developer explicitly opted in for the given host — the plugin already maintains a clientCertificateIdentities map that could serve as that opt-in.

Code Reproduction

  1. Create a fresh Capacitor 8 app, install @capgo/inappbrowser, and call:
await InAppBrowser.openWebView({ url: 'https://<test-server>' });
  1. Point it at any HTTPS server configured with optional client certificate verification, e.g.:
  • nginx: ssl_verify_client optional;
  • ASP.NET Core Kestrel: ClientCertificateMode.AllowCertificate
  1. On the device, install any user client certificate (Settings → Security → Encryption & credentials → Install a certificate → VPN and app user certificate), or use a Samsung device which has keystore entries out of the box.
  2. Open the page — the system certificate picker appears immediately during page load. The same page opened in the same app with plugin v7.x loads silently.

Other Technical Details

npm --version output: 10.9.8

node --version output: v22.22.3

pod --version output (iOS issues only): n/a (Android-only issue)

Additional Context

  • Real-world trigger: PolCard / Fiserv Poland payment gateway (ssl.dotpay.pl:443) sends an optional TLS CertificateRequest; our users on Samsung devices get the certificate dialog during rent payments.
  • Workaround we currently ship: patch-package replacing the method body with request.cancel().
  • Happy to submit a PR adding the opt-in option if you agree on the API shape.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions