Abstract:
This post details the journey of building the Android frontend for FIDO2 passkey authentication. It covers tackling the CredentialManager
API, migrating local data storage from Proto DataStore to Preferences DataStore, and the debugging strategies employed, including Logcat and in-app debug displays, all within a Jetpack Compose environment.
Estimated reading time: 5 minutes
Android Passkeys: Frontend Adventures with CredentialManager and Jetpack Compose
Following up on my previous post about backend challenges with FIDO2 passkeys, this time I'm diving into the Android frontend. Building the UI and logic for passkey authentication in our “People App” (a fork of Google's “Now In Android” sample) was an interesting journey, especially when wrestling with Jetpack Compose, the CredentialManager
API, and a necessary data storage overhaul.
Starting Point: The “Now In Android” Scaffold
We kicked off by forking the “Now In Android” (NIA) app. It's a fantastic resource, showcasing modern Android development best practices, including a sophisticated build system using build-logic
, convention plugins, and version catalogs. This gave us a solid foundation but also meant an initial learning curve (as seen in commits like d11f29c0
and ed9761de
where we were renaming packages and stripping out NIA-specifics).
One of the first significant architectural decisions we made was around local data storage.
DataStore Decisions: Proto vs. Preferences
NIA uses Proto DataStore for user preferences (like theme settings or whether onboarding has been completed), managed via NiaPreferencesDataSource.kt
and UserPreferencesSerializer.kt
. This is powerful, offering type safety and structured data.
However, for the simpler key-value settings we anticipated for People App's UI state, Proto DataStore felt a bit like overkill. We decided to switch to Jetpack Preferences DataStore.
This involved:
- Creating a new
PeepsPreferencesDataSource.kt
. - Removing the Proto DataStore files and its serializers/migrations.
- Updating our
DataStoreModule.kt
(Hilt/Dagger) to provide the newPeepsPreferencesDataSource
instead of the old one. - Adjusting
UserDataRepository
(and its test doubles likeTestUserDataRepository.kt
) to use the new Preferences DataStore.
The core/datastore/build.gradle.kts
file reflected this change clearly:
1
2
3
4
// Example changes in core/datastore/build.gradle.kts
- implementation(libs.androidx.datastore.core) // Or similar if it was specifically for Proto
- implementation(libs.protobuf.javalite)
+ implementation(libs.androidx.datastore.preferences)
This shift simplified how we handled simple UI flags, making the code a bit more straightforward for those specific use cases.
Building the Passkey UI with Jetpack Compose
With the data layer reconfigured, the main event was building the passkey authentication screens: LoginScreen.kt
and its corresponding LoginViewModel.kt
.
Key Frontend Responsibilities:
- Initiating
CredentialManager
operations: The ViewModel became the orchestrator forgetCredential
(for sign-in) andcreatePublicKeyCredential
(for registration) requests. - Managing UI State: This was crucial. We needed to handle various states in Compose:
- Idle (ready for user input).
- Loading (while
CredentialManager
or our backend is working). - Success (leading to navigation, e.g., to
PeopleScreen.kt
). - Error (displaying issues like no passkeys found, network errors, or
GetCredentialException
subtypes).
- Handling System Dialogs: A lot of the passkey UX is handled by system-provided dialogs. Our job on the frontend was to correctly trigger these via
PendingIntent
and then gracefully process the results returned inonActivityResult
(or its modern equivalents).
The Joys of Debugging CredentialManager
Ah, CredentialManager
. Powerful, but sometimes opaque. When passkey operations failed, especially with generic errors like NoCredentialException
(or TYPE_NO_CREDENTIAL
as it sometimes manifests), it was time to get friendly with debugging tools.
1. Logcat, My Old Friend:
Logcat was indispensable. For those new to Android development or needing a quick reminder, Logcat is typically found at the bottom of Android Studio, as shown here:
Fig 1: The Logcat tab in Android Studio, your window into the device's soul (and your app's chatter).
Filtering for tags like CredentialManager
, Passkey
, or even broader system service tags often provided clues. Were there underlying Play Services issues? Was the rpId
subtly mismatched between what the server expected and what the client was configured with? Logcat often held the answers, even if they were buried in verbose output. This was particularly key when the backend challenges around assetlinks.json
were still being ironed out, as client errors were the first sign of trouble.
2. The Invaluable In-App Debug Display:
To get a clearer real-time picture of what was happening, especially with the data being exchanged, I ended up building a simple on-screen debug overlay within the app. This temporary UI would display:
* The current rpId
being used for the request.
* The raw challenge string received from our server (before Base64URL decoding).
* The credentialId
(s) being sent in allowCredentials
during login attempts.
* The status or error code from the last CredentialManager
operation.
This immediate visual feedback was a lifesaver. It allowed us to quickly spot discrepancies in the JSON request options being prepared for CredentialManager
calls, verifying data before it went into the “black box” of the system API or off to our backend. It saved countless hours that would have otherwise been spent stepping through the debugger or just staring at Logcat.
Looking Back
Migrating the NIA scaffold, making key architectural choices like the DataStore switch, and then building the passkey UI with Jetpack Compose was a significant effort. The CredentialManager
API, while a huge step forward for Android authentication, requires careful handling on the frontend to manage its asynchronous nature and provide a clear, robust user experience. And never underestimate the power of good old Logcat and a quick-and-dirty debug overlay when you're in the trenches!
**Disclaimer - Google Gemini 2.5 Pro Exp was used to help write this blog post.