Why Passkeys Are Replacing Passwords
Setting Up Credential Manager
// build.gradle.kts (app module)
dependencies {
implementation("androidx.credentials:credentials:1.5.0")
implementation("androidx.credentials:credentials-play-services-auth:1.5.0")
implementation("com.google.android.libraries.identity.googleid:googleid:1.1.1")
}
// AuthRepository.kt
class AuthRepository @Inject constructor(
@ActivityContext private val context: Context,
private val api: AuthApi,
) {
private val credentialManager = CredentialManager.create(context)
// We'll use this in the registration and authentication sections below
}Passkey Registration: Creating Credentials
suspend fun registerPasskey(userId: String): Result<Unit> {
// Step 1: Get registration options from your backend
val options = api.getRegistrationOptions(userId)
// Step 2: Create the credential request
val request = CreatePublicKeyCredentialRequest(
requestJson = options.toJson() // WebAuthn PublicKeyCredentialCreationOptions JSON
)
return try {
// Step 3: System shows biometric prompt + creates keypair
val response = credentialManager.createCredential(
context = context as Activity,
request = request,
)
// Step 4: Send attestation to backend for validation
val publicKeyResponse = response as CreatePublicKeyCredentialResponse
api.verifyRegistration(
userId = userId,
attestation = publicKeyResponse.registrationResponseJson
)
Result.success(Unit)
} catch (e: CreateCredentialCancellationException) {
Result.failure(AuthError.UserCancelled)
} catch (e: CreateCredentialProviderConfigurationException) {
Result.failure(AuthError.PlayServicesUnavailable)
} catch (e: CreateCredentialException) {
Result.failure(AuthError.RegistrationFailed(e.message))
}
}Passkey Authentication: Signing In
suspend fun signIn(): Result<AuthSession> {
// Step 1: Get authentication challenge from backend
val challenge = api.getAuthenticationOptions()
// Step 2: Build request with multiple credential types
val passKeyOption = GetPublicKeyCredentialOption(
requestJson = challenge.toJson()
)
val googleIdOption = GetGoogleIdOption.Builder()
.setFilterByAuthorizedAccounts(false)
.setServerClientId(BuildConfig.GOOGLE_WEB_CLIENT_ID)
.setAutoSelectEnabled(true)
.build()
val request = GetCredentialRequest.Builder()
.addCredentialOption(passKeyOption)
.addCredentialOption(googleIdOption)
.build()
return try {
// Step 3: System shows unified credential picker
val response = credentialManager.getCredential(
context = context as Activity,
request = request,
)
// Step 4: Handle the credential type returned
when (val credential = response.credential) {
is PublicKeyCredential -> {
val session = api.verifyAuthentication(
assertion = credential.authenticationResponseJson
)
Result.success(session)
}
is CustomCredential -> {
if (credential.type == GoogleIdTokenCredential.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL) {
val googleId = GoogleIdTokenCredential.createFrom(credential.data)
val session = api.verifyGoogleToken(googleId.idToken)
Result.success(session)
} else {
Result.failure(AuthError.UnsupportedCredentialType)
}
}
else -> Result.failure(AuthError.UnsupportedCredentialType)
}
} catch (e: GetCredentialCancellationException) {
Result.failure(AuthError.UserCancelled)
} catch (e: NoCredentialException) {
Result.failure(AuthError.NoCredentialsAvailable)
} catch (e: GetCredentialException) {
Result.failure(AuthError.AuthenticationFailed(e.message))
}
}ViewModel Integration and UI Flow
@HiltViewModel
class AuthViewModel @Inject constructor(
private val authRepository: AuthRepository,
) : ViewModel() {
sealed interface AuthState {
data object Idle : AuthState
data object Loading : AuthState
data class Success(val session: AuthSession) : AuthState
data class Error(val message: String, val canRetry: Boolean) : AuthState
}
private val _state = MutableStateFlow<AuthState>(AuthState.Idle)
val state = _state.asStateFlow()
fun signIn() {
viewModelScope.launch {
_state.value = AuthState.Loading
authRepository.signIn().fold(
onSuccess = { session -> _state.value = AuthState.Success(session) },
onFailure = { error ->
_state.value = when (error) {
is AuthError.UserCancelled -> AuthState.Idle
is AuthError.NoCredentialsAvailable -> AuthState.Error(
"No saved credentials. Create an account first.",
canRetry = false
)
else -> AuthState.Error(
error.message ?: "Authentication failed",
canRetry = true
)
}
}
)
}
}
fun registerPasskey(userId: String) {
viewModelScope.launch {
_state.value = AuthState.Loading
authRepository.registerPasskey(userId).fold(
onSuccess = { _state.value = AuthState.Idle }, // Passkey saved
onFailure = { error ->
_state.value = AuthState.Error(
error.message ?: "Passkey registration failed",
canRetry = true
)
}
)
}
}
}Security Considerations and Production Checklist
Key Takeaways
- 1Passkeys eliminate shared secrets — the private key never leaves the device, making phishing and credential stuffing impossible.
- 2Credential Manager unifies passkeys, saved passwords, and Google Sign-In into one API call with a system-provided bottom sheet.
- 3Registration is a two-step flow: backend generates WebAuthn options, Credential Manager handles biometric prompt and key generation.
- 4Authentication supports multiple credential types simultaneously — the system presents the best available option to the user.
- 5Server-side challenges must be cryptographically random, single-use, and time-limited. Never generate challenges on the client.
- 6Maintain password-based auth as a fallback during transition. Prompt users to create passkeys when they sign in with passwords.
Frequently Asked
Why are passkeys more secure than passwords?
There is no shared secret to steal. The private key never leaves the device, which makes phishing and credential-stuffing attacks structurally impossible rather than merely harder.
Can I generate the WebAuthn challenge on the client?
No. Challenges must be cryptographically random, single-use, and time-limited, and they must be generated server-side. A client-generated challenge defeats the protocol.
Should I remove password login once passkeys ship?
Not during the transition. Keep password auth as a fallback and prompt users to create a passkey when they sign in with a password.
Ready to architect your next Android app?
ANDROID-ARCHITECT generates production-ready Kotlin code, architecture blueprints, and CI/CD configurations from plain-language descriptions. Start building for free.