Android Passwordless Authentication: Implementing Passkeys with MVVM
Passkeys are the modern, more secure alternative to passwords. Based on the FIDO2/WebAuthn standard, they allow users to sign in to apps and websites using their device’s screen lock (fingerprint, face, or PIN). Unlike passwords, passkeys are unique to every account and never leave the user’s device.
In this guide, we’ll walk through implementing Passkey authentication in Android using the Credential Manager API and MVVM architecture.
🏗️ High-Level Design (HLD)
Passkey authentication involves a two-step handshake between the app and the backend server for both registration and login.
Passkey Authentication Flow Diagram:
🛠️ Implementation Steps
1. Add Dependencies
Get Rajendrakumar Dabhi’s stories in your inbox
Join Medium for free to get updates from this writer.
Add the Credential Manager and Play Services Auth libraries to your build.gradle.kts:
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")
}2. The Passkey Authenticator Logic
The PasskeyAuthenticator encapsulates the CredentialManager calls. It handles the JSON interaction required for FIDO2.
class PasskeyAuthenticator(context: Context) {
private val credentialManager = CredentialManager.create(context)
suspend fun registerPasskey(activity: FragmentActivity, requestJson: String): Result<CreateCredentialResponse> {
return try {
val request = CreatePublicKeyCredentialRequest(requestJson)
val result = credentialManager.createCredential(activity, request)
Result.success(result)
} catch (e: CreateCredentialException) {
Result.failure(e)
}
}
suspend fun loginWithPasskey(activity: FragmentActivity, requestJson: String): Result<GetCredentialResponse> {
return try {
val option = GetPublicKeyCredentialOption(requestJson)
val request = GetCredentialRequest(listOf(option))
val result = credentialManager.getCredential(activity, request)
Result.success(result)
} catch (e: GetCredentialException) {
Result.failure(e)
}
}
}3. ViewModel Integration (MVVM)
The ViewModel handles the state transition and communicates with the backend to fetch challenges.
sealed class PasskeyUiState {
object Idle : PasskeyUiState()
object Loading : PasskeyUiState()
data class Success(val msg: String) : PasskeyUiState()
data class Error(val error: String) : PasskeyUiState()
}
class PasskeyViewModel(private val authenticator: PasskeyAuthenticator) : ViewModel() {
private val _uiState = MutableStateFlow<PasskeyUiState>(PasskeyUiState.Idle)
val uiState: StateFlow<PasskeyUiState> = _uiState.asStateFlow()
fun login(activity: FragmentActivity) {
viewModelScope.launch {
_uiState.value = PasskeyUiState.Loading
// 1. Fetch challenge from your backend
val challengeJson = repository.getLoginChallenge()
// 2. Trigger Credential Manager
val result = authenticator.loginWithPasskey(activity, challengeJson)
// 3. Verify assertion with backend
result.onSuccess {
repository.verifyLoginAssertion(it)
_uiState.value = PasskeyUiState.Success("Welcome back!")
}.onFailure {
_uiState.value = PasskeyUiState.Error(it.message ?: "Login failed")
}
}
}
}4. UI with Jetpack Compose
@Composable
fun PasskeyScreen(viewModel: PasskeyViewModel) {
val uiState by viewModel.uiState.collectAsState()
Card(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Column(modifier = Modifier.padding(24.dp)) {
Text("Passkey Authentication", style = MaterialTheme.typography.headlineSmall)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { viewModel.login(activity) },
enabled = uiState !is PasskeyUiState.Loading
) {
Text("Sign in with Passkey")
}
}
}
}🔐 Why Passkeys are Better
1.Phishing Resistant: Passkeys are bound to the domain (RP ID). A user cannot accidentally “type” their passkey into a fake website.
2.No More Passwords: Users don’t need to remember anything. They use the same biometric flow they use to unlock their phone.
3.Sync-able: Passkeys are backed up to the cloud (e.g., Google Password Manager) and work across all the user’s Android devices.
🚀 Conclusion
The shift towards a passwordless world is happening now. By integrating Passkeys into your Android app, you provide a world-class sign-in experience that is faster, easier, and significantly more secure than traditional credentials.
Start building the passwordless future today! 🔑🚀






