Beyond Passwords: Implementing Verified Email with Android Credential Manager
Account creation drop-off is one of the most critical pain points for mobile application growth. Every time a user is forced to leave your app, open their email client, wait for a One-Time Password (OTP), memorize it, and switch back to your app to type it in, you risk losing that user forever. Friction is the enemy of conversion.
With the latest updates to the Android Credential Manager API, developers have a powerful new tool at their disposal: the ability to retrieve cryptographically verified email addresses directly from trusted providers (like Google), completely eliminating the manual OTP verification step.
In this article, we will explore how to implement this secure, OTP-less email verification flow using Digital Credentials.
The Magic of Digital Credentials
This new flow utilizes the Digital Credentials Verifier API and relies on the OpenID for Verifiable Presentations (OpenID4VP) standard.
When a user taps “Sign Up”, instead of being asked to manually type an email and wait for a code, they are presented with a secure, system-level bottom sheet. This prompt asks the user to consent to sharing their verified email address. Behind the scenes, the system securely fetches this information, wrapped in a cryptographic token.
1. Constructing the Request
To ask the system for a verified email, we must construct a JSON request. This request asks for a UserInfoCredential and specifically requests the email_verified claim.
Here is what the OpenID4VP JSON request looks like in Kotlin:
val nonce = generateSecureRandomNonce() // Essential for preventing replay attacks
val openId4vpRequest = """
{
"requests": [
{
"protocol": "openid4vp-v1-unsigned",
"data": {
"response_type": "vp_token",
"response_mode": "dc_api",
"nonce": "$nonce",
"dcql_query": {
"credentials": [
{
"id": "user_info_query",
"format": "dc+sd-jwt",
"meta": {
"vct_values": ["UserInfoCredential"]
},
"claims": [
{"path": ["email"]},
{"path": ["name"]},
{"path": ["email_verified"]}
]
}
]
}
}
}
]
}
"""
The email_verified claim is the key here. If the response returns true for this claim, you have cryptographic proof that the email belongs to the user.
2. Requesting the Credential via Credential Manager
Once we have our JSON string, we wrap it in a GetDigitalCredentialOption and initiate the request via the CredentialManager.
val credentialManager = CredentialManager.create(context)
val getDigitalCredentialOption = GetDigitalCredentialOption(requestJson = openId4vpRequest)
val request = GetCredentialRequest(listOf(getDigitalCredentialOption))
try {
// This triggers the secure system UI asking for user consent
val result = credentialManager.getCredential(activity, request)
when (val credential = result.credential) {
is DigitalCredential -> {
val responseJsonString = credential.credentialJson
// Successfully received digital credential response.
// DO NOT trust this data locally yet!
sendToServerForValidation(responseJsonString, nonce)
}
else -> {
// Handle unexpected credential types
}
}
} catch (e: GetCredentialException) {
// Handle user cancellation or errors
}
3. Critical Step: Server-Side Cryptographic Validation
This is the most important part of the implementation: You must never trust the raw data parsed on the client side for account creation.
The Credential Manager returns a Selective Disclosure JWT (SD-JWT). A malicious actor could easily modify a local client to return fake JSON. To ensure security, you must send the entire responseJsonString, along with the original nonce, to your backend server.
Your server must perform the following validation:
- Verify the Issuer: Ensure the
iss(issuer) field matches the trusted provider (e.g.,https://verifiablecredentials-pa.googleapis.com). - Verify the Signature: Check the signature of the SD-JWT using the provider’s public keys to prove the data was genuinely issued by them and hasn’t been tampered with.
- Verify the Nonce: Ensure the
noncematches what was originally requested and hasn’t been used before, preventing replay attacks. - Verify the Presenter: Verify the
cnf(confirmation) field to ensure the credential is being shared by the same device it was originally issued to.
Only after the server successfully validates these cryptographic constraints should the user account be provisioned.
4. The Perfect Follow-Up: Passkey Creation
Once your server has validated the email and provisioned the account, you have a verified user. However, they don’t have a password.
This is the perfect moment to immediately trigger Credential Manager’s Passkey Creation flow.
// Pseudo-code for immediately offering a Passkey
if (accountCreatedSuccessfully) {
val passkeyRequest = CreatePublicKeyCredentialRequest(fetchPasskeyChallengeFromServer())
credentialManager.createCredential(activity, passkeyRequest)
}
By doing this, you have seamlessly onboarded a user without them ever typing a password or checking an OTP, and you’ve secured their future logins with a biometric Passkey.
Conclusion
Implementing verified email via the Android Credential Manager fundamentally transforms the user onboarding experience. By replacing high-friction manual verification with secure, cryptographic digital credentials, you can significantly reduce drop-off rates, increase conversion, and elevate the security posture of your Android applications.