Many users still manage their own credentials when setting up a new Android-powered device. This manual process can become challenging and often results in a poor user experience. The Block Store API, a library powered by Google Play services, looks to solve this by providing a way for apps to save user credentials without the complexity or security risk associated with saving user passwords.
The Block Store API allows your app to store data that it can later retrieve to re-authenticate users on a new device. This helps provide a more seamless experience for the user, as they don't need to see a sign-in screen when launching your app for the first time on the new device.
The benefits to using Block Store include the following:
- Encrypted credential storage solution for developers. Credentials are end-to-end encrypted when possible.
- Save tokens instead of usernames and passwords.
- Eliminate friction from sign-in flows.
- Save users from the burden of managing complex passwords.
- Google verifies the user's identity.
Before you begin
To prepare your app, complete the steps in the following sections.
Configure your app
In your project-level build.gradle file, include Google's Maven
repository in both your buildscript
and allprojects sections:
buildscript {
repositories {
google()
mavenCentral()
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
Add the Google Play services dependency for the Block Store API
to your module's Gradle build file, which is commonly app/build.gradle:
dependencies {
implementation 'com.google.android.gms:play-services-auth-blockstore:16.4.0'
}
How it works
Block Store allows developers to save and restore up to 16 byte arrays. This lets you save important information regarding the current user session and offers the flexibility to save this information however you like. This data can be end-to-end encrypted and the infrastructure that supports Block Store is built on top of the Backup and Restore infrastructure.
This guide will cover the use case of saving a user's token to Block Store. The following steps outline how an app utilizing Block Store would work:
- During your app’s authentication flow, or anytime thereafter, you can store the user’s authentication token to Block Store for later retrieval.
- The token will be stored locally and can also be backed up to the cloud, end-to-end encrypted when possible.
- Data is transferred when the user initiates a restore flow on a new device.
- If the user restores your app during the restore flow, your app can then retrieve the saved token from Block Store on the new device.
Saving the token
When a user signs into your app, you can save the authentication token that you
generate for that user to Block Store. You can store this token using a unique
key pair value that has a maximum 4kb per entry. To store the token, call
setBytes() and setKey() on an instance of
StoreBytesData.Builder to store the user's credentials to the source
device. After you save the token with Block Store, the token is encrypted and
stored locally on the device.
The following sample shows how to save the authentication token to the local device:
Java
BlockstoreClient client = Blockstore.getClient(this); byte[] bytes1 = new byte[] { 1, 2, 3, 4 }; // Store one data block. String key1 = "com.example.app.key1"; StoreBytesData storeRequest1 = StoreBytesData.Builder() .setBytes(bytes1) // Call this method to set the key value pair the data should be associated with. .setKeys(Arrays.asList(key1)) .build(); client.storeBytes(storeRequest1) .addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes")) .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this) val bytes1 = byteArrayOf(1, 2, 3, 4) // Store one data block. val key1 = "com.example.app.key1" val storeRequest1 = StoreBytesData.Builder() .setBytes(bytes1) // Call this method to set the key value with which the data should be associated with. .setKeys(Arrays.asList(key1)) .build() client.storeBytes(storeRequest1) .addOnSuccessListener { result: Int -> Log.d(TAG, "Stored $result bytes") } .addOnFailureListener { e -> Log.e(TAG, "Failed to store bytes", e) }
Use default token
Data saved using StoreBytes without a key uses the default key
BlockstoreClient.DEFAULT_BYTES_DATA_KEY.
Java
BlockstoreClient client = Blockstore.getClient(this); // The default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY. byte[] bytes = new byte[] { 9, 10 }; StoreBytesData storeRequest = StoreBytesData.Builder() .setBytes(bytes) .build(); client.storeBytes(storeRequest) .addOnSuccessListener(result -> Log.d(TAG, "stored " + result + " bytes")) .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this); // the default key BlockstoreClient.DEFAULT_BYTES_DATA_KEY. val bytes = byteArrayOf(1, 2, 3, 4) val storeRequest = StoreBytesData.Builder() .setBytes(bytes) .build(); client.storeBytes(storeRequest) .addOnSuccessListener { result: Int -> Log.d(TAG, "stored $result bytes") } .addOnFailureListener { e -> Log.e(TAG, "Failed to store bytes", e) }
Retrieving the token
Later on, when a user goes through the restore flow on a new device, Google Play
services first verifies the user, then retrieves your Block Store data. The user
has already agreed to restore your app data as a part of the restore flow, so no
additional consents are required. When the user opens your app, you can request
your token from Block Store by calling retrieveBytes(). The retrieved
token can then be used to keep the user signed in on the new device.
The following sample shows how to retrieve multiple tokens based on specific keys.
Java
BlockstoreClient client = Blockstore.getClient(this); // Retrieve data associated with certain keys. String key1 = "com.example.app.key1"; String key2 = "com.example.app.key2"; String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to retrieve data stored without a key ListrequestedKeys = Arrays.asList(key1, key2, key3); // Add keys to array RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder() .setKeys(requestedKeys) .build(); client.retrieveBytes(retrieveRequest) .addOnSuccessListener( result -> { Map<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap(); for (Map.Entry<String, BlockstoreData> entry : blockstoreDataMap.entrySet()) { Log.d(TAG, String.format( "Retrieved bytes %s associated with key %s.", new String(entry.getValue().getBytes()), entry.getKey())); } }) .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this) // Retrieve data associated with certain keys. val key1 = "com.example.app.key1" val key2 = "com.example.app.key2" val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array val retrieveRequest = RetrieveBytesRequest.Builder() .setKeys(requestedKeys) .build() client.retrieveBytes(retrieveRequest) .addOnSuccessListener { result: RetrieveBytesResponse -> val blockstoreDataMap = result.blockstoreDataMap for ((key, value) in blockstoreDataMap) { Log.d(ContentValues.TAG, String.format( "Retrieved bytes %s associated with key %s.", String(value.bytes), key)) } } .addOnFailureListener { e: Exception? -> Log.e(ContentValues.TAG, "Failed to store bytes", e) }
Retrieving all tokens.
Below is an example of how to retrieve all the tokens saved to BlockStore.
Java
BlockstoreClient client = Blockstore.getClient(this) // Retrieve all data. RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder() .setRetrieveAll(true) .build(); client.retrieveBytes(retrieveRequest) .addOnSuccessListener( result -> { Map<String, BlockstoreData> blockstoreDataMap = result.getBlockstoreDataMap(); for (Map.Entry<String, BlockstoreData> entry : blockstoreDataMap.entrySet()) { Log.d(TAG, String.format( "Retrieved bytes %s associated with key %s.", new String(entry.getValue().getBytes()), entry.getKey())); } }) .addOnFailureListener(e -> Log.e(TAG, "Failed to store bytes", e));
Kotlin
val client = Blockstore.getClient(this) val retrieveRequest = RetrieveBytesRequest.Builder() .setRetrieveAll(true) .build() client.retrieveBytes(retrieveRequest) .addOnSuccessListener { result: RetrieveBytesResponse -> val blockstoreDataMap = result.blockstoreDataMap for ((key, value) in blockstoreDataMap) { Log.d(ContentValues.TAG, String.format( "Retrieved bytes %s associated with key %s.", String(value.bytes), key)) } } .addOnFailureListener { e: Exception? -> Log.e(ContentValues.TAG, "Failed to store bytes", e) }
Below is an example of how to retrieve the default key.
Java
BlockStoreClient client = Blockstore.getClient(this); RetrieveBytesRequest retrieveRequest = new RetrieveBytesRequest.Builder() .setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY)) .build(); client.retrieveBytes(retrieveRequest);
Kotlin
val client = Blockstore.getClient(this) val retrieveRequest = RetrieveBytesRequest.Builder() .setKeys(Arrays.asList(BlockstoreClient.DEFAULT_BYTES_DATA_KEY)) .build() client.retrieveBytes(retrieveRequest)
Deleting tokens
Deleting tokens from BlockStore may be required for the following reasons:
- User goes through sign out user flow.
- Token has been revoked or is invalid.
Similar to retrieving tokens, you can specify which tokens need deleting by setting an array of keys which require deletion.
The following example demonstrates how to delete certain keys:
Java
BlockstoreClient client = Blockstore.getClient(this); // Delete data associated with certain keys. String key1 = "com.example.app.key1"; String key2 = "com.example.app.key2"; String key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY; // Used to delete data stored without key ListrequestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array DeleteBytesRequest deleteRequest = new DeleteBytesRequest.Builder() .setKeys(requestedKeys) .build(); client.deleteBytes(deleteRequest)
Kotlin
val client = Blockstore.getClient(this) // Retrieve data associated with certain keys. val key1 = "com.example.app.key1" val key2 = "com.example.app.key2" val key3 = BlockstoreClient.DEFAULT_BYTES_DATA_KEY // Used to retrieve data stored without a key val requestedKeys = Arrays.asList(key1, key2, key3) // Add keys to array val retrieveRequest = DeleteBytesRequest.Builder() .setKeys(requestedKeys) .build() client.deleteBytes(retrieveRequest)
Delete All Tokens
The following example shows how to delete all the tokens currently saved to BlockStore: