Skip to content

Commit 46c3fc3

Browse files
author
Niharika Arora
committed
Biometric(Single tap passkey creation & sign-in) implementation in MyVault Provider
1 parent 1c9ac15 commit 46c3fc3

File tree

17 files changed

+862
-235
lines changed

17 files changed

+862
-235
lines changed

CredentialProvider/MyVault/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ The app demonstrates how to:
1414
- Register as a `CredentialProviderService` so that users can store and retrieve passwords and passkeys using the app.
1515
- Save passwords/passkeys to the app. These are stored locally in a database for demonstration purposes only. In a real app this data should be sent to a server to allow the user's credentials to be synchronized across all their devices.
1616
- Retrieve credentials from the app to assist with user login in another app or website.
17+
- Implement your own biometrics prompt(single tap credential creation & sign-in)
1718
- Delete passkeys or passwords.
1819

1920
# Requirements
@@ -104,6 +105,9 @@ These additional activities are described below.
104105

105106
For more detailed information on how to create, save and retrieve credentials using the Credential Manager API, refer to the [official documentation]((https://developer.android.com/training/sign-in/credential-provider))
106107

108+
To implement your own biometrics prompt, refer to the [biometrics documentation](https://developer.android.com/identity/sign-in/single-tap-biometric)
109+
110+
107111
## License
108112

109113
The **MyVault Sample** is distributed under the terms of the Apache License (Version 2.0).

CredentialProvider/MyVault/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,12 @@ plugins {
2222
android {
2323
namespace = "com.example.android.authentication.myvault"
2424

25-
compileSdkPreview = "VanillaIceCream"
25+
compileSdk = 35
2626

2727
defaultConfig {
2828
applicationId = "com.example.android.authentication.myvault"
2929
minSdk = 34
30-
targetSdkPreview = "VanillaIceCream"
30+
targetSdk= 35
3131
versionCode = 1
3232
versionName = "1.0"
3333

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/AppDependencies.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ object AppDependencies {
4040

4141
var providerIcon: Icon? = null
4242

43-
lateinit var RPIconDataSource: RPIconDataSource
43+
lateinit var rpIconDataSource: RPIconDataSource
4444

4545
/**
4646
* Initializes the core components required for the application's data storage and icon handling.
@@ -63,7 +63,7 @@ object AppDependencies {
6363
.fallbackToDestructiveMigration()
6464
.build()
6565

66-
RPIconDataSource = RPIconDataSource(context.applicationInfo.dataDir)
66+
rpIconDataSource = RPIconDataSource(context.applicationInfo.dataDir)
6767
providerIcon = Icon.createWithResource(context, R.drawable.android_secure)
6868

6969
credentialsRepository =

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/CredentialsRepository.kt

Lines changed: 150 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import android.app.PendingIntent
1919
import android.content.Context
2020
import android.content.Intent
2121
import android.content.SharedPreferences
22+
import android.hardware.biometrics.BiometricManager
23+
import android.os.Build
2224
import android.os.Bundle
2325
import androidx.credentials.provider.BeginCreateCredentialRequest
2426
import androidx.credentials.provider.BeginCreateCredentialResponse
@@ -28,6 +30,7 @@ import androidx.credentials.provider.BeginGetCredentialRequest
2830
import androidx.credentials.provider.BeginGetCredentialResponse.Builder
2931
import androidx.credentials.provider.BeginGetPasswordOption
3032
import androidx.credentials.provider.BeginGetPublicKeyCredentialOption
33+
import androidx.credentials.provider.BiometricPromptData
3134
import androidx.credentials.provider.CreateEntry
3235
import androidx.credentials.provider.PasswordCredentialEntry
3336
import androidx.credentials.provider.PublicKeyCredentialEntry
@@ -39,14 +42,18 @@ import java.time.Instant
3942
import java.util.concurrent.atomic.AtomicInteger
4043

4144
/**
42-
* This class is responsible for creating and retrieving credential entries (password & passkey) to & from the database
45+
* Manages the creation and retrieval of credential entries (passwords and passkeys)
46+
* to and from the MyVault Provider. This class also configures the biometric prompt
47+
* for Android API level 35 and higher.
4348
*/
4449
class CredentialsRepository(
4550
private val sharedPreferences: SharedPreferences,
4651
private val credentialsDataSource: CredentialsDataSource,
4752
private val applicationContext: Context,
4853
) {
4954
private val requestCode: AtomicInteger = AtomicInteger()
55+
private val allowedAuthenticator =
56+
BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG or BiometricManager.Authenticators.DEVICE_CREDENTIAL
5057

5158
/**
5259
* This method queries credentials from your database, create passkey and password entries to populate.
@@ -153,19 +160,20 @@ class CredentialsRepository(
153160
val passwordItemCurrent = it.next()
154161

155162
// Create Password entry
156-
val entry = PasswordCredentialEntry.Builder(
157-
applicationContext,
158-
passwordItemCurrent.username,
159-
createNewPendingIntent(
160-
passwordItemCurrent.username,
161-
GET_PASSWORD_INTENT,
162-
),
163-
option,
164-
)
165-
.setDisplayName("display-${passwordItemCurrent.username}")
166-
.setIcon(AppDependencies.providerIcon!!)
167-
.setLastUsedTime(Instant.ofEpochMilli(passwordItemCurrent.lastUsedTimeMs))
168-
.build()
163+
val entryBuilder =
164+
configurePasswordCredentialEntryBuilder(passwordItemCurrent, option)
165+
166+
// Configure own biometric prompt data
167+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
168+
entryBuilder.setBiometricPromptData(
169+
BiometricPromptData(
170+
cryptoObject = null,
171+
allowedAuthenticators = allowedAuthenticator,
172+
),
173+
)
174+
}
175+
176+
val entry = entryBuilder.build()
169177
// Add the entry to the response builder.
170178
responseBuilder.addCredentialEntry(entry)
171179
}
@@ -175,6 +183,32 @@ class CredentialsRepository(
175183
return true
176184
}
177185

186+
/**
187+
* Configures a {@link PasswordCredentialEntry.Builder} for a given password item.
188+
*
189+
* @param passwordItemCurrent The {@link PasswordItem} containing the password details.
190+
* @param option The {@link BeginGetPasswordOption} containing the request parameters.
191+
* @return A {@link PasswordCredentialEntry.Builder} configured with the provided
192+
* password details and request options.
193+
*/
194+
private fun configurePasswordCredentialEntryBuilder(
195+
passwordItemCurrent: PasswordItem,
196+
option: BeginGetPasswordOption,
197+
): PasswordCredentialEntry.Builder {
198+
val entryBuilder = PasswordCredentialEntry.Builder(
199+
applicationContext,
200+
passwordItemCurrent.username,
201+
createNewPendingIntent(
202+
passwordItemCurrent.username,
203+
GET_PASSWORD_INTENT,
204+
),
205+
option,
206+
).setDisplayName("display-${passwordItemCurrent.username}")
207+
.setIcon(AppDependencies.providerIcon!!)
208+
.setLastUsedTime(Instant.ofEpochMilli(passwordItemCurrent.lastUsedTimeMs))
209+
return entryBuilder
210+
}
211+
178212
/**
179213
* This method queries credentials from your database, create passkey and password entries to populate.
180214
*
@@ -207,18 +241,21 @@ class CredentialsRepository(
207241
)
208242

209243
// Create a PublicKeyCredentialEntry object to represent the passkey
210-
val entryBuilder = PublicKeyCredentialEntry.Builder(
211-
applicationContext,
212-
passkey.username,
213-
pendingIntent,
214-
option,
215-
)
216-
.setDisplayName(passkey.displayName)
217-
.setLastUsedTime(Instant.ofEpochMilli(passkey.lastUsedTimeMs))
218-
.setIcon(AppDependencies.providerIcon!!)
244+
val entryBuilder =
245+
configurePublicKeyCredentialEntryBuilder(passkey, pendingIntent, option)
219246

220-
val entry = entryBuilder
221-
.build()
247+
// Configure biometric prompt data
248+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
249+
entryBuilder.setBiometricPromptData(
250+
BiometricPromptData(
251+
cryptoObject = null,
252+
allowedAuthenticators = allowedAuthenticator,
253+
),
254+
)
255+
}
256+
257+
val entry = entryBuilder.build()
258+
// Add the entry to the response builder.
222259
responseBuilder.addCredentialEntry(entry)
223260
}
224261
} catch (e: IOException) {
@@ -227,6 +264,33 @@ class CredentialsRepository(
227264
return true
228265
}
229266

267+
/**
268+
* Creates a {@link PublicKeyCredentialEntry.Builder} for a given passkey.
269+
*
270+
* @param passkey The {@link PasskeyItem} containing the passkey details.
271+
* @param pendingIntent The {@link PendingIntent} to be associated with the entry,
272+
* used to launch the passkey retrieval process.
273+
* @param option The {@link BeginGetPublicKeyCredentialOption} containing the
274+
* request parameters for the passkey retrieval.
275+
* @return A {@link PublicKeyCredentialEntry.Builder} configured with the provided
276+
* passkey details, pending intent, and request options.
277+
*/
278+
private fun configurePublicKeyCredentialEntryBuilder(
279+
passkey: PasskeyItem,
280+
pendingIntent: PendingIntent,
281+
option: BeginGetPublicKeyCredentialOption,
282+
): PublicKeyCredentialEntry.Builder {
283+
val entryBuilder = PublicKeyCredentialEntry.Builder(
284+
applicationContext,
285+
passkey.username,
286+
pendingIntent,
287+
option,
288+
).setDisplayName(passkey.displayName)
289+
.setLastUsedTime(Instant.ofEpochMilli(passkey.lastUsedTimeMs))
290+
.setIcon(AppDependencies.providerIcon!!)
291+
return entryBuilder
292+
}
293+
230294
/**
231295
* Creates a new PendingIntent for the given action and account ID.
232296
*
@@ -261,15 +325,18 @@ class CredentialsRepository(
261325
}
262326

263327
/**
264-
* Adds a CreateEntry to the BeginCreateCredentialResponse.
328+
* Handles the creation of a credential query response.
265329
*
266-
* Each CreateEntry should correspond to an account where the credential can be saved,
267-
* and must have a PendingIntent set along with other required metadata.
330+
* <p>This method constructs a {@link BeginCreateCredentialResponse} that
331+
* includes a {@link CreateEntry} for an account where credentials can be
332+
* saved. The {@link CreateEntry} contains a {@link PendingIntent} and other
333+
* metadata required for the credential creation process.
268334
*
269335
* @param passwordCount The number of password credentials associated with the account.
270-
* @param passkeyCount The number of passkey credentials associated with the account.
271-
* @param intentType The type of intent to be used for the PendingIntent.
272-
* @return A BeginCreateCredentialResponse with the CreateEntry added.
336+
* @param passkeyCount The number of passkey credentials associated with the account.
337+
* @param intentType The type of intent to be used for the {@link PendingIntent}.
338+
* @return A {@link BeginCreateCredentialResponse} containing the created
339+
* {@link CreateEntry}.
273340
*/
274341
private fun handleCreateCredentialQuery(
275342
passwordCount: Int,
@@ -278,46 +345,69 @@ class CredentialsRepository(
278345
): BeginCreateCredentialResponse {
279346
// Each CreateEntry should correspond to an account where the credential can be saved,
280347
// and must have a PendingIntent set along with other required metadata.
281-
return BeginCreateCredentialResponse.Builder()
282-
.addCreateEntry(
283-
createEntry(
284-
intentType,
285-
passwordCount,
286-
passkeyCount,
287-
),
288-
).build()
348+
349+
// Create the CreateEntry using the provided parameters.
350+
val createCredentialEntry = createEntry(
351+
intentType,
352+
passwordCount,
353+
passkeyCount,
354+
)
355+
// Build and return the BeginCreateCredentialResponse with the created CreateEntry.
356+
return BeginCreateCredentialResponse.Builder().addCreateEntry(
357+
createCredentialEntry,
358+
).build()
289359
}
290360

291361
/**
292-
* Creates a CreateEntry object for the user account based on their credential preferences.
362+
* Creates a {@link CreateEntry} object for the user account.
363+
*
364+
* <p>This method constructs a {@link CreateEntry} that represents an account
365+
* where credentials can be saved. It sets various properties of the
366+
* {@link CreateEntry}, including the account identifier, a {@link PendingIntent}
367+
* for credential creation, the last used time, the number of password and
368+
* passkey credentials, the total credential count, and a description.
369+
* Additionally, it configures biometric prompt data if the device is running
370+
* Android API level 35 or higher.
293371
*
294-
* @param intentType The type of intent to be used for the PendingIntent.
372+
* @param intentType The type of intent to be used for the {@link PendingIntent}.
295373
* @param passwordCount The number of password credentials associated with the account.
296-
* @param passkeyCount The number of passkey credentials associated with the account.
297-
* @return A CreateEntry object.
374+
* @param passkeyCount The number of passkey credentials associated with the account.
375+
* @return A {@link CreateEntry} object configured with the specified parameters.
298376
*/
299377
private fun createEntry(
300378
intentType: String,
301379
passwordCount: Int,
302380
passkeyCount: Int,
303-
) = CreateEntry.Builder(
304-
USER_ACCOUNT,
305-
createNewPendingIntent(USER_ACCOUNT, intentType),
306-
).setLastUsedTime(
307-
Instant.ofEpochMilli(
308-
sharedPreferences.getLong(
309-
KEY_ACCOUNT_LAST_USED_MS,
310-
0L,
381+
): CreateEntry {
382+
// Create a CreateEntry.Builder with the user account and a PendingIntent.
383+
val createEntryBuilder = CreateEntry.Builder(
384+
USER_ACCOUNT,
385+
createNewPendingIntent(USER_ACCOUNT, intentType),
386+
).setLastUsedTime(
387+
Instant.ofEpochMilli(
388+
sharedPreferences.getLong(
389+
KEY_ACCOUNT_LAST_USED_MS,
390+
0L,
391+
),
311392
),
312-
),
313-
)
314-
.setPasswordCredentialCount(passwordCount)
315-
.setPublicKeyCredentialCount(passkeyCount)
316-
.setTotalCredentialCount(passwordCount + passkeyCount)
317-
.setDescription(
318-
CREDENTIAL_DESCRIPTION,
319-
)
320-
.build()
393+
).setPasswordCredentialCount(passwordCount).setPublicKeyCredentialCount(passkeyCount)
394+
.setTotalCredentialCount(passwordCount + passkeyCount).setDescription(
395+
CREDENTIAL_DESCRIPTION,
396+
)
397+
398+
// Configure biometric prompt data if the device is running Android API level 35 or higher.
399+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
400+
createEntryBuilder.setBiometricPromptData(
401+
BiometricPromptData(
402+
cryptoObject = null,
403+
allowedAuthenticators = allowedAuthenticator,
404+
),
405+
)
406+
}
407+
408+
// Build and return the CreateEntry.
409+
return createEntryBuilder.build()
410+
}
321411

322412
companion object {
323413
private const val CREATE_PASSWORD_INTENT =

CredentialProvider/MyVault/app/src/main/java/com/example/android/authentication/myvault/data/MyVaultService.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,6 @@ import java.util.concurrent.atomic.AtomicInteger
5252
class MyVaultService(private val credentialsRepository: CredentialsRepository = AppDependencies.credentialsRepository) :
5353
CredentialProviderService() {
5454

55-
5655
/**
5756
* Called by the Android System in response to a client app calling
5857
* [androidx.credentials.CredentialManager.createCredential], to create/save a credential

0 commit comments

Comments
 (0)