Orchestration SDKs

Customizing DaVinci module storage on Android

PingOne Android

Depending on the authentication use case, you may need to store and retrieve ID tokens, access tokens, refresh tokens, or cookies.

Each token is serving a different use case, and as such how you handle them can be different.

The Orchestration SDKs employ identity best practices for storing data by default. However there are use cases where you might need to customize how the SDK stores data.

For example, you might be running on hardware that provides specialized security features, or perhaps target older hardware that cannot handle the latest algorithms.

For these cases, you can customize the provided storage solutions, or provide your own custom storage classes.

Add dependencies

To customize your storage solution you need to add the storage module to your project.

Adding dependencies

To add the storage module dependencies to your Android project:

  1. In the Project tree view of your Android Studio project, open the Gradle Scripts/build.gradle.kts file for the module.

  2. In the dependencies section, add the required dependencies:

    Example dependencies section after editing build.gradle.kts:
    dependencies {
      // DaVinci orchestration module
      implementation("com.pingidentity.sdks:davinci:2.0.0")
    
      // Storage module
      implementation("com.pingidentity.sdks:storage:2.0.0")
    }

Using the provided storage solutions

You can use the default storage solutions and configure them to suit your requirements.

Provided default storage solutions

You can use the default storage solutions in your apps, depending on the type of data you want to store.

MemoryStorage

Storage that stores data in memory.

Data stored using the MemoryStorage solution is kept in plain text and is not encrypted.

A device that can output a memory dump may expose sensitive information, such as access or ID tokens.

DataStoreStorage

Storage backed by Jetpack DataStore.

Data stored using the DataStoreStorage solution is kept in plain text and is not encrypted.

EncryptedDataStoreStorage

Encrypted version of the DataStoreStorage solution, also backed by Jetpack DataStore.

All SDK modules use this solution by default

Configuring storage solutions

You can customize aspects of the storage solutions by passing parameters when creating a storage instance.

On Android, you can configure the EncryptedDataStoreStorage storage solution with the following properties:

Android EncryptedDataStoreStorage properties
Property Description

cacheStrategy

Enable caching of the data.

Learn more in Enabling caching.

Default value

CacheStrategy.NO_CACHE

fileName

The name of the file used for persistent storage.

Default values
OpenID Connect token storage

com.pingidentity.sdk.v1.tokens

Cookie storage

com.pingidentity.sdk.v1.cookies

keyAlias

The string used as the alias for the key. When provided, enables encryption using AndroidKeyStore.

You can use any value that does not clash with any other key names. A common pattern is <top-level-domain>.<company-name>.<version>.KEYS.

For example, com.example.v1.KEYS.

Default values
OpenID Connect token storage

com.pingidentity.sdk.v1.tokens

Cookie storage

com.pingidentity.sdk.v1.cookies

strongBoxPreferred

When true the storage module attempts to use hardware-backed StrongBox functionality for key storage, if available on the client device.

Some devices implement StrongBox, but are not optimal. You can use the Build class to conditionally apply the strongBoxPreferred flag based on the device manufacturer, model, or other properties.

Default value

false

The following code shows examples of customizing storage solutions:

Customizing the EncryptedDataStoreStorage storage solution
module(Oidc) {
    clientId = "6c7eb89a-66e9-ab12-cd34-eeaf795650b2"
    discoveryEndpoint = "https://auth.pingone.ca/3072206d-c6ce-ch15-m0nd-f87e972c7cc3/as/.well-known/openid-configuration"

    // OpenID Connect storage configuration options
    storage {
        fileName = "myOidcTokens"
        keyAlias = "com.example.v1.KEYS"
        strongBoxPreferred = true
        cacheStrategy = CacheStrategy.CACHE_ON_FAILURE
    }
}
module(Cookie) {
    // The cookie name to persist
    persist = mutableListOf("ST", "ST-NO-SS")
    // Cookie storage configuration options
    storage {
        strongBoxPreferred = false
    }
}

Enabling caching

You can add caching to each storage solution depending on the requirements of the type of data you store.

Data stored in a cache is kept in plain text and is not encrypted.

A device that can output a memory dump may expose sensitive information, such as access or ID tokens.

Use the cacheStrategy property when creating a storage instance to configure the type of cache the storage uses. The available options are as follows:

CacheStrategy.NO_CACHE

The default for each storage solution.

Prevents caching and always fetches data from storage.

Use for critical data that must always be up-to-date.

CacheStrategy.CACHE_ON_FAILURE

Caches data in memory if the storage operation fails.

Use to overcome storage interruptions, and allow fallback data reads.

CacheStrategy.CACHE

Always caches data in memory.

Use for non-critical, but highly performant data reads.

Example:

Creating a storage instance with cache enabled
val storage = DataStoreStorage<String> {
    fileName = "com.pingidentity.sdk.v1.tokens"
    cacheStrategy = CacheStrategy.CACHE
}

Implementing your own custom storage

You can create your own custom storage solutions by implementing the Storage interface. For example, you could implement a file-based or cloud-based storage solution.

You must implement the following functions in each storage class:

save()

Stores an item in the customized storage.

get()

Retrieves an item from the customized storage.

delete()

Removes an item from the customized storage.

The Storage interface on Android
class Memory<T : @Serializable Any> : Storage<T> {
  private var data: T? = null

  override suspend fun save(item: T?) {
    data = item
  }

  override suspend fun get(): T? = data

  override suspend fun delete() {
    data = null
  }
}

// Delegate the MemoryStorage to the Storage
inline fun <reified T : @Serializable Any> MemoryStorage(): Storage<T> = StorageDelegate(Memory())

Use your custom storage solution in a module as follows:

Using a custom storage solution
module(Cookie) {
    persist = mutableListOf("ST", "ST-NO-SS")
    storage = { MemoryStorage() }
}

Use an equals sign when assigning a custom class to the storage property in the module configuration.

You do not need an equals sign when passing configuration settings to the default storage solution for a module.