Last month, one of our product teams watched something painful in real time. A login funnel that usually converted fine during office hours started dropping users hard after 8 PM. We traced sessions, devices, geographies, and network errors. The pattern was embarrassingly simple: users were abandoning at OTP entry. They were tired, switching between apps, waiting on delayed SMS, and retrying until they gave up.
That week forced a practical decision, not a “future roadmap” one: we had to move from fragile OTP-heavy auth to passkeys. This guide is the playbook we ended up using for Android passkeys with Credential Manager in an existing app, without ripping out our entire auth stack on day one.
If you are already working on security posture work, these pieces pair well with our earlier write-ups on zero-trust engineering hardening, FIDO2-backed admin access, and production guardrails from secure tool ecosystems. The same reliability mindset from event-driven backend systems applies here too.
The technical shift, in plain terms
Passkeys replace shared secrets (passwords, OTP codes) with public-key cryptography. Your server stores a public key, not a reusable secret. The user authenticates locally on the device, and the authenticator signs a challenge from your server. This is why passkeys are much stronger against phishing and credential stuffing than password + OTP flows.
From the implementation side, Android’s Credential Manager gives you one UI surface for passkeys, passwords, and federated options. That matters for migration because you can phase passkeys in without forcing every user through a big-bang auth rewrite.
Tradeoffs you should plan for
- Migration takes product work, not just SDK work. You need recovery flows, messaging, and fallback handling for older devices.
- Server correctness is non-negotiable. Weak challenge generation or improper origin/RP ID validation cancels the security benefits.
- You should keep at least one fallback temporarily. During rollout, maintain a hardened fallback path while measuring real adoption.
Implementation blueprint for Android + backend
Below is the shape that worked for us in a legacy app with existing JWT sessions.
1) Android client: request and create credentials via Credential Manager
import androidx.credentials.CredentialManager
import androidx.credentials.GetCredentialRequest
import androidx.credentials.GetPasswordOption
import androidx.credentials.GetPublicKeyCredentialOption
import androidx.credentials.CreatePublicKeyCredentialRequest
class AuthRepository(private val credentialManager: CredentialManager) {
suspend fun signInWithPasskey(activity: android.app.Activity, requestJson: String): String {
val passkeyOption = GetPublicKeyCredentialOption(
requestJson = requestJson // WebAuthn request options from server
)
val fallbackPassword = GetPasswordOption() // temporary migration fallback
val request = GetCredentialRequest(
credentialOptions = listOf(passkeyOption, fallbackPassword)
)
val result = credentialManager.getCredential(
context = activity,
request = request
)
// Send result.credential.data (JSON payload) to server for verification
return result.credential.data.toString()
}
suspend fun registerPasskey(activity: android.app.Activity, creationJson: String): String {
val createRequest = CreatePublicKeyCredentialRequest(
requestJson = creationJson // WebAuthn creation options from server
)
val createResult = credentialManager.createCredential(
context = activity,
request = createRequest
)
// Send attestation response to server
return createResult.data.toString()
}
}
The key discipline: Android should never invent security-sensitive values client-side. Challenge, RP information, and allowed credentials should come from the server every time.
2) Server side: strict WebAuthn verification
For Node.js backends, a common path is using @simplewebauthn/server and storing each credential’s public key + counter per user.
import {
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from '@simplewebauthn/server';
export async function verifyPasskeyAuthentication({
response,
expectedChallenge,
credentialFromDb,
}) {
const verification = await verifyAuthenticationResponse({
response,
expectedChallenge,
expectedOrigin: 'https://app.example.com',
expectedRPID: 'example.com',
credential: {
id: credentialFromDb.credentialId,
publicKey: credentialFromDb.publicKey,
counter: credentialFromDb.counter,
transports: credentialFromDb.transports || [],
},
requireUserVerification: true,
});
if (!verification.verified) {
throw new Error('Passkey verification failed');
}
// Rotate challenge and update signature counter atomically
await db.credentials.updateCounter(
credentialFromDb.credentialId,
verification.authenticationInfo.newCounter,
);
return { verified: true, userId: credentialFromDb.userId };
}
For WebAuthn server verification, the minimum bar is: short-lived challenge storage, RP ID + origin validation, replay prevention, and atomic counter updates.
A rollout plan that does not explode support tickets
Our most useful insight was this: implementation is only half the work. The other half is your passkey rollout strategy.
- Stage 0, instrumentation first: add analytics for current login friction (OTP delays, retries, abandonment).
- Stage 1, opt-in passkeys for active users: prompt users after successful login, not before.
- Stage 2, passkeys as preferred sign-in: reorder UI to make passkey first while fallback remains visible.
- Stage 3, risk-based fallback: keep fallback for recovery/high-risk edge cases, not as the default happy path.
When teams skip staged rollout, they usually overestimate “technical completion” and underestimate behavior change. Clear copy helps. “Use your phone unlock to sign in faster” consistently converted better than jargon-heavy security messaging.
What to measure in the first 30 days
Do not judge rollout health by raw passkey registrations alone. That metric is easy to inflate and easy to misread. Track whether passkeys improve real sign-in outcomes compared to your baseline password or OTP path.
- Sign-in success rate by platform version to catch compatibility pockets early.
- Median time-to-authenticate to validate UX gains instead of guessing.
- Fallback usage trend to understand migration maturity and copy clarity.
- Recovery-trigger rate to detect lockout risk and fraud pressure.
- Support ticket tags for sign-in confusion, device change, and account recovery.
This is where phishing-resistant authentication becomes measurable engineering value, not just a security slide. If success rate rises while fallback and ticket volume drop, your rollout is working.
Troubleshooting: what usually breaks first
1) “No credential available” on eligible devices
Usually means no passkey was registered for that RP ID, or account matching is off. Validate RP IDs and user mapping before blaming the SDK.
2) Verification fails even though UI looked successful
Check expectedOrigin and expectedRPID. Most failures come from environment mismatch (staging domain vs production RP ID).
3) Replay or stale challenge errors
Challenges should be one-time and short-lived. If retries reuse old values, reject and issue a fresh challenge.
4) Counter mismatch for returning users
Update signature counters transactionally. Race conditions in parallel sign-ins can trigger false positives if updates are not atomic.
5) Recovery flow causes lockouts
Do not remove fallback too early. Keep account recovery verified with strong checks, and log every recovery event for abuse review.
FAQ
Q1) Do passkeys completely remove the need for passwords today?
Not immediately for every user segment. In practice, most teams run a hybrid period while adoption matures across devices and user habits.
Q2) Is biometric data sent to our servers when users sign in with passkeys?
No. Device biometrics stay on-device. Your server receives signed credential assertions, not raw biometric material.
Q3) Can passkeys work with existing account systems?
Yes. You can link passkeys to existing accounts, keep fallback auth temporarily, and gradually shift default sign-in to passkeys based on measured adoption.
Actionable takeaways for this sprint
- Pick one Android app flow and ship passkey registration behind a feature flag this week.
- Implement challenge expiry + replay protection before UI polish.
- Log verification failure reasons (origin, RP ID, challenge, counter) with redacted payloads for fast debugging.
- Run a controlled rollout to a small cohort and compare sign-in success against OTP baseline.
- Keep fallback auth, but demote it in UI once passkey success rates stabilize.
Passkeys are not just a security upgrade, they are an operations upgrade. If your team is battling OTP drop-offs and account takeover pressure at the same time, Android passkeys with Credential Manager are one of the rare changes that improve both UX and risk posture when implemented with discipline.

Leave a Reply