@mxwmnn/capacitor-android-webauthn
A Capacitor plugin that polyfills the WebAuthn navigator.credentials API on Android using Android Credential Manager. This lets passkeys work in Capacitor WebViews on Android 14+ or Android 9+ with Play Services.
This implementation is based on Google's official sample: https://github.com/android/identity-samples/tree/main/WebView/CredentialManagerWebView
- Auto-initialization: The plugin automatically loads when your app starts
- JavaScript Injection: Injects a polyfill that intercepts
navigator.credentials.create()andnavigator.credentials.get()calls - Native Bridge: Routes WebAuthn requests through Android's Credential Manager API
- Zero Configuration: No changes needed to MainActivity or your app code
┌─────────────────────────────────────┐
│ JavaScript (WebView) │
│ navigator.credentials.create() │
│ navigator.credentials.get() │
└──────────────┬──────────────────────┘
│ (polyfilled by encode.js)
▼
┌─────────────────────────────────────┐
│ PasskeyWebListener (Kotlin) │
│ - Receives postMessage from JS │
│ - Validates origin & request │
└──────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ CredentialManagerHandler (Kotlin) │
│ - Calls Android Credential Manager │
│ - Returns result to WebView │
└─────────────────────────────────────┘
android/src/main/java/io/github/mxwmnn/webauthn/CredentialManagerPlugin.kt- Main Capacitor plugin entry pointPasskeyWebListener.kt- Handles WebView message communicationCredentialManagerHandler.kt- Wraps Android Credential Manager APIReplyChannel.kt- Interface for sending messages back to WebView
javascript/encode.js- Non-minified polyfill (reference only)- This gets minified and embedded in
PasskeyWebListener.INJECTED_VAL - Intercepts
navigator.credentialscalls - Communicates with native code via
postMessage
- This gets minified and embedded in
src/definitions.ts- TypeScript interfacessrc/index.ts- Main exportsrc/web.ts- Web platform stub (no-op, uses native WebAuthn)
This plugin targets Capacitor v8 and Android only.
npm install @mxwmnn/capacitor-android-webauthnOr with pnpm:
pnpm i @mxwmnn/capacitor-android-webauthnOr with Bun:
bun add @mxwmnn/capacitor-android-webauthnThis repository uses Bun for local development workflows.
Install dependencies:
bun installBuild the plugin:
bun run buildRun Android verification:
bun run verifybun run verify uses the Gradle wrapper checked into android/, so it does not depend on a system Gradle installation.
The plugin works automatically! Just use the standard WebAuthn API:
// Create a passkey (registration)
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array([/* challenge bytes */]),
rp: { name: "My App", id: "example.com" },
user: {
id: new Uint8Array([/* user id */]),
name: "user@example.com",
displayName: "User"
},
pubKeyCredParams: [{ type: "public-key", alg: -7 }],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required"
}
}
});
// Authenticate with a passkey
const credential = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array([/* challenge bytes */]),
rpId: "example.com",
userVerification: "required"
}
});The plugin exposes debug methods for troubleshooting:
import { CredentialManager } from '@mxwmnn/capacitor-android-webauthn';
// Check if polyfill is injected
const { isInjected } = await CredentialManager.checkInjection();
console.log('Polyfill injected:', isInjected);
// Manually re-inject polyfill (shouldn't be needed)
await CredentialManager.injectPolyfill();
// Test plugin communication
const { value } = await CredentialManager.echo({ value: 'test' });
console.log('Echo:', value);View logs with:
adb logcat | grep -E "CredentialManager|PasskeyWebListener"Expected logs on startup:
I CredentialManagerPlugin: CredentialManagerPlugin loading...
D CredentialManagerPlugin: Setting up WebView injection...
I CredentialManagerPlugin: Injecting WebAuthn polyfill into current page...
I CredentialManagerPlugin: WebMessageListener registered successfully
I CredentialManagerPlugin: WebView injection setup complete
I CredentialManagerPlugin: CredentialManagerPlugin loaded successfully
I CredentialManagerPlugin: Initial polyfill injection completed: null
When a passkey request is made:
I PasskeyWebListener: In Post Message : {"type":"create","request":{...}} source: https://...
-
"WebAuthn not permitted for current URL"
- URL must be HTTPS (not HTTP or localhost)
- Main frame only (iframes not supported)
-
No logs from PasskeyWebListener
- Check that polyfill is injected:
CredentialManager.checkInjection() - Verify WebMessageListener is registered in logs
- Check that polyfill is injected:
-
Polyfill not working
- The polyfill injects immediately when the plugin loads
- It should work on the first page load
- Use
injectPolyfill()to manually re-inject if needed
The JavaScript code in javascript/encode.js is the human-readable version. It gets minified and embedded in PasskeyWebListener.kt as INJECTED_VAL.
To update the polyfill:
- Edit
javascript/encode.js - Minify it using the toptal minifier:
cat javascript/encode.js | grep -v '^let __webauthn_interface__;$' | \ curl -X POST --data-urlencode input@- \ https://www.toptal.com/developers/javascript-minifier/api/raw | tr '"' "'"
- Copy the output and replace
INJECTED_VALinPasskeyWebListener.kt
Note: The grep removes the let __webauthn_interface__; line which is declared in the minified output.
- 8.0.0 - First public release under the
@mxwmnnscope for Capacitor v8.
- Android 14+ (API level 34+) or Android 9+ (API Level 28+) with Play Services
- AndroidX Credentials library
- AndroidX WebKit library
- HTTPS origin (for production use)
MIT. The implementation is based in part on Google's Apache 2.0 licensed Credential Manager WebView sample.