Compose Multiplatform passkey sample¶
A Compose Multiplatform app for a minimal end-to-end passkey flow against sample/backend-ktor.
What this demonstrates¶
- Runtime capability probing via
PasskeyCapabilities.supports(...)(PRF extension, Large Blob extension, security key support). - End-to-end passkey registration against
POST /webauthn/registration/start+/webauthn/registration/finish. - End-to-end passkey sign-in against
POST /webauthn/authentication/start+/webauthn/authentication/finish. - Two-screen auth/session flow:
Authscreen (Register,Sign In) and signed-in extension demo screen with local logout transition back toAuth. - Compose-first authentication wiring via
rememberPasskeyFlow(...), with sample-owned state and errors driving UI status and action availability. - Direct sample wiring to
KotlinxKtorPasskeyBackendagainst the default backend contract. - PRF crypto demo flow: caller-owned salt loading or generation,
Sign In + PRF, session-key derivation, AES-GCM encryption and decryption, and explicit session clearing. - Explicit
Logsaction in the shared header opening an in-app debug log sheet (wall-clock timestamps, level, source, message). - Structured ceremony + network logs emitted with tag
PasskeyDemo.
These values are baked into the shared app during build:
WEBAUTHN_DEMO_ENDPOINT(default:http://127.0.0.1:8080)WEBAUTHN_DEMO_RP_ID(default:localhost)WEBAUTHN_DEMO_ORIGIN(iOS/web origin; default:https://localhost)WEBAUTHN_DEMO_USER_ID(default:demo-user-1)WEBAUTHN_DEMO_USER_NAME(default:demo@local)WEBAUTHN_DEMO_UNSAFE_HTTP_BODY_LOGGING(default:false)- Android host only:
WEBAUTHN_DEMO_REQUEST_LOCAL_NETWORK_PERMISSION(default:false)
The Android host does not use WEBAUTHN_DEMO_ORIGIN. Credential Manager sets an
Android app origin from the SHA-256 fingerprint of the installed app's signing
certificate, so the host derives the matching android:apk-key-hash:... value at
runtime and injects it into the shared sample.
Examples:
- Android Emulator host alias:
WEBAUTHN_DEMO_ENDPOINT=http://10.0.2.2:8080 - Physical phone on LAN:
WEBAUTHN_DEMO_ENDPOINT=http://<laptop-lan-ip>:8080 - ngrok tunnel:
WEBAUTHN_DEMO_ENDPOINT=https://<domain>and setWEBAUTHN_DEMO_RP_ID/WEBAUTHN_DEMO_ORIGINto the same HTTPS domain for the iOS/web association; Android derives its app origin from its signing certificate
Run (Android)¶
Android requirement: API level 30+ (minSdk 30) for the PRF crypto sample flow.
- Start sample backend:
For physical devices, prefer tunnel mode:
This updates root local.properties (WEBAUTHN_DEMO_ENDPOINT,
WEBAUTHN_DEMO_RP_ID, WEBAUTHN_DEMO_ORIGIN) to match the active ngrok domain.
It also synchronizes the Android signing fingerprint used by Digital Asset Links;
the Android app derives its ceremony origin from the installed signing certificate.
Android 17 note: the Android host targets SDK 37, so direct private-network endpoints
such as 10.0.2.2, 192.168.x.x, or 172.16-31.x.x require the platform
ACCESS_LOCAL_NETWORK runtime permission. Set
WEBAUTHN_DEMO_REQUEST_LOCAL_NETWORK_PERMISSION=true when building the sample
against one of those direct local endpoints. Public HTTPS endpoints and loopback
defaults do not need the prompt.
- Build and run sample host app:
WEBAUTHN_DEMO_ENDPOINT=http://10.0.2.2:8080 \
WEBAUTHN_DEMO_REQUEST_LOCAL_NETWORK_PERMISSION=true \
./gradlew :sample:compose-passkey-android:installDebug
- Optional UI smoke test (emulator/device connected):
Run (iOS host app)¶
Use the committed iOS host project:
Quick start:
- Open
sample/compose-passkey-ios/ComposePasskeyIos.xcodeprojin Xcode. - Set your signing team and a unique bundle ID.
- Connect your iPhone and run the app.
This shared module still exports the Compose entrypoint used by the host app:
dev.webauthn.samples.composepasskey.MainViewController()
Free-account expectation:
- App install/launch is supported.
- Passkey registration and sign-in may fail when the Associated Domains entitlement or domain association is unavailable.
Full E2E expectation:
- Use an HTTPS domain, Associated Domains, and a matching
IOS_APP_IDand bundle identity. sample/backend-ktor/start-server.sh(ngrok helper) remains the default physical-device setup path.
Debug logging¶
The sample emits structured logs with tag PasskeyDemo and uses the same entries for the in-app debug sheet:
app: startup and configurationcapabilities: probe start/success/failureaction: register/sign-in tapsprf: PRF sign-in/session/encrypt/decrypt outcomesflow: state transitions (STARTING,PLATFORM_PROMPT,FINISHING, terminal outcomes)http: Ktor request/response metadata (method, URL, and status)
HTTP bodies are excluded by default because WebAuthn responses and PRF extension
values contain sensitive material. For an explicit local debugging session, build
with WEBAUTHN_DEMO_UNSAFE_HTTP_BODY_LOGGING=true to switch the Ktor logger to
LogLevel.BODY. This escape hatch performs no redaction; disable it before sharing
logs or distributing a build.
To inspect logs:
- Android:
adb logcat | grep PasskeyDemo - iOS: Xcode/device console output (
NSLog) - In-app: tap
Logsin the header on either screen to open the debug sheet.
Auth route showcase¶
The auth screen is intentionally the cleanest API example in the repo:
val flow = rememberPasskeyFlow(passkeyClient)
val scope = rememberCoroutineScope()
val coordinator = remember(config, debugLogs, sessionStore) {
AuthDemoCoordinator(config, debugLogs, sessionStore)
}
var state by remember { mutableStateOf<DemoCeremonyState>(DemoCeremonyState.Idle) }
val canRegister by coordinator.canRegister.collectAsState()
val actionsEnabled = areCeremonyActionsEnabled(state)
Sample-only side effects stay outside the library API surface:
AuthDemoCoordinatorlogs taps/state transitions.AppSessionStorehandles local signed-in navigation state.
Compose previews¶
Preview catalog composables live in common source:
src/commonMain/kotlin/dev/webauthn/samples/composepasskey/ui/previews/ScreenPreviewCatalog.ktsrc/commonMain/kotlin/dev/webauthn/samples/composepasskey/ui/previews/ComponentPreviewCatalog.kt
Preview limitations and constraints:
- Previews are static and fake-state only; they must stay free of DI (
koin*), network clients, and platform runtime calls. - Interactive runtime flows (Navigation 3 back stack, passkey platform prompts, live bottom-sheet gestures) are not fully represented in preview mode.
- Android Studio rendering still relies on Android target preview tooling, so
androidMainincludescompose.ui.toolingfor this module. - Treat previews as UI contract checks, not behavioral verification; lifecycle/interop behavior must still be validated via tests and host-app runs.
Test layering (fake vs real client)¶
- Generic flow behavior is covered in
webauthn-client-flow; the sample owns its presentation/error union. - Runtime client wiring uses
webauthn-client-compose(rememberPasskeyClient()+rememberPasskeyFlow()). - Runtime server wiring uses
webauthn-client-ktor-kotlinx(KotlinxKtorPasskeyBackend). - Final readiness still requires the live registration and sign-in checklist on a physical Android device or emulator with provider dependencies present.
Android provider prerequisite¶
The Android host includes androidx.credentials:credentials-play-services-auth, but real passkey prompts still require:
- Google Play-enabled emulator/device.
- Screen lock configured.
- A passkey-capable account/provider on the device.
If provider wiring is missing at runtime, the sample surfaces an actionable hint in the status UI and debug log.
Practical passkey note¶
For realistic device passkey prompts, use HTTPS plus associated-domain configuration:
- Android:
/.well-known/assetlinks.json - iOS:
/.well-known/apple-app-site-association
sample/backend-ktor serves both endpoints.
PRF crypto safety note¶
- Salt persistence is intentionally caller-owned and sample-local (
InMemoryPrfSaltStore) to keep library storage-independent. - Encrypted payloads are tied to passkey PRF output. If the passkey credential is removed, previously encrypted data cannot be recovered.