Ping SDKs

Customize storage

Applies to:

  • Ping SDK for Android

  • Ping SDK for iOS

  • Ping SDK for JavaScript

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

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

The SDKs employ identity best practices for storing data by default. To learn more about how the SDKs store different data, refer to Token and key security and Data security.

There are use cases where you might need to customize how to store 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 provide your own storage classes.

Customize storage on Android

You can configure your Android apps to use customized storage for these types of data:

  1. OAuth 2.0 / OpenID Connect 1.0 tokens

  2. SSO data

  3. Cookies

Depending on why you want to override storage mechanisms, you might prefer instead to prevent use of StrongBox.

Implement storage override classes

Use the Storage interface to override the different types of storage as follows

OpenID Connect storage

Storage<AccessToken>

SSO token storage

Storage<SSOToken>

Cookie storage

Storage<Collection<String>>

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.

Examples:

class MyCustomTokenStorage(context: Context) : Storage<AccessToken> {

    override fun save(item: AccessToken) {
        TODO("Implement save to storage functionality")
    }

    override fun get(): AccessToken? {
        TODO("Implement retrieve to storage functionality")
    }

    override fun delete() {
        TODO("Implement remove from storage functionality")
    }
}
kotlin

The SDK includes a basic example of a customized storage class that places data temporarily in memory. Refer to MemoryStorage.kt in the forgerock-android-sdk GitHub repo.

Apps you release that use customized storage will not be able to access existing data that was stored using a different method.

Previous users of your app will have to log in again after upgrading to an app that is using a different storage mechanism.

To prevent having to log in again your custom storage could manually migrate any existing data to the new storage during initialization.

For an example of migrating existing stored data, see SSOTokenStorage.kt

Configure storage overrides

Add a store key to the FROptionsBuilder.build parameters to specify which storage types to override, and the class you created above that provides the implementation:

val options = FROptionsBuilder.build {
    server {
       url = ""
       realm = ""
       cookieName = ""
    }
    oauth {
       oauthClientId = ""
       oauthRedirectUri = "/callback"
       oauthScope = ""
    }
    service {
       authServiceName = "Login"
       registrationServiceName = "Registration"
    }
    store {
      // Default storage settings
      // Uses SecureSharedPreferences
      //   oidcStorage = TokenStorage(ContextProvider.context)
      //   ssoTokenStorage = SSOTokenStorage(ContextProvider.context)
      //   cookiesStorage = CookiesStorage(ContextProvider.context)

      oidcStorage = MyCustomTokenStorage(ContextProvider.context)
      ssoTokenStorage = MyCustomSSOTokenStorage(ContextProvider.context)
      cookiesStorage = MyCustomCookiesStorage(ContextProvider.context)
    }
}

FRAuth.start(this, options);
kotlin

You can only specify the store options when dynamically configuring the Ping SDK for Android.

You cannot add the parameters to the strings.xml file.

Implement storage fallbacks

One use case for providing custom storage is when the device you are targeting might not support the default SecureSharedPreferences storage methods provided by the SDK.

In this case you can create a fallback mechanism such that if the default storage method produces an error, a second storage method attempts to save the data.

The following CustomStorageWithFallback.kt example file is available in the forgerock-android-sdk GitHub repo.

package com.example.app.storage

import android.content.Context
import kotlinx.serialization.Serializable
import org.forgerock.android.auth.AccessToken
import org.forgerock.android.auth.SSOToken
import org.forgerock.android.auth.storage.CookiesStorage
import org.forgerock.android.auth.storage.SSOTokenStorage
import org.forgerock.android.auth.storage.Storage
import org.forgerock.android.auth.storage.TokenStorage

/
 * A custom storage implementation that switches to a fallback storage when an error occurs.
 */
class CustomStorageWithFallback<T : @Serializable Any>(
    private val context: Context,
    private val flag: String, (1)
    primary: Storage<T>, (2)
    private val fallback: Storage<T> (3)
) : Storage<T> {

    @Volatile
    private var current: Storage<T> = primary (4)

    /
     * Save an item to the current storage. If an error occurs, switch to the fallback storage.
     *
     * @param item The item to be saved.
     */
    override fun save(item: T) {
        try {
            // Save the item to the current storage.
            current.save(item) (5)
        } catch (e: Throwable) {
            // If an error occurs, switch to the fallback storage.
            context.getSharedPreferences("storage-control", Context.MODE_PRIVATE).edit()
                .putInt(flag, 1).apply() (6)
            fallback.save(item) (7)
            current = fallback
        }
    }

    /
     * Retrieve an item from the current storage.
     *
     * @return The retrieved item, or null if no item is found.
     */
    override fun get(): T? {
        return current.get()
    }

    /
     * Delete an item from the current storage.
     */
    override fun delete() {
        current.delete()
    }
}

/
 * Load the SSO token storage with a fallback mechanism.
 *
 * @param context The application context.
 * @return The storage instance for SSO tokens.
 */
fun loadSSOTokenStorage(context: Context): Storage<SSOToken> {  (8)
    return loadStorage(
        context,
        "ssoStorage",
        { SSOTokenStorage(context) },
        { MemoryStorage() }
    )
}

/
 * Load the token storage with a fallback mechanism.
 *
 * @param context The application context.
 * @return The storage instance for tokens.
 */
fun loadTokenStorage(context: Context): Storage<AccessToken> { (9)
    return loadStorage(
        context,
        "tokenStorage",
        { TokenStorage(context) },
        { MemoryStorage() }
    )
}

/
 * Load the cookies storage with a fallback mechanism.
 *
 * @param context The application context.
 * @return The storage instance for cookies.
 */
fun loadCookiesStorage(context: Context): Storage<Collection<String>> { (10)
    return loadStorage(
        context,
        "cookiesStorage",
        { CookiesStorage(context) },
        { MemoryStorage() }
    )
}

/
 * Load a storage instance with a fallback mechanism.
 *
 * @param T The type of object to be stored.
 * @param context The application context.
 * @param flag A flag used to control the storage type.
 * @param primary A function to initialize the primary storage.
 * @param fallback A function to initialize the fallback storage.
 * @return The storage instance.
 */
inline fun <reified T : Any> loadStorage( (11)
    context: Context,
    flag: String,
    primary: () → Storage<T>,
    fallback: () → Storage<T>
): Storage<T> {
    val control = context.getSharedPreferences("storage-control", Context.MODE_PRIVATE)
    // Get the storage type from the control flag. 0: primary, 1: fallback.
    val storageType = control.getInt(flag, 0)
    return when (storageType) {
        // Use the primary storage.
        0 → CustomStorageWithFallback(context,
            flag,
            primary(),
            fallback())

        // Use the fallback storage.
        else → fallback()
    }
}
kotlin
1 Flag whether the code should use the primary storage mechanism, or the fallback
2 The class to use as the primary storage mechanism
3 The class to use as the fallback storage mechanism
4 Initially, set the primary mechanism as current
5 Attempt to save with the current mechanism
6 If it fails, set flag to 1
7 Attempt to save with the fallback mechanism
8 Create an SSO token wrapper function to load the primary and fallback mechanisms
9 Create an OIDC token wrapper function to load the primary and fallback mechanisms
10 Create a Cookie wrapper function to load the primary and fallback mechanisms
11 Create a function to load the customized storage wrappers

Configure your SDK application as follows to use the customized storage with fallback functionality:

store {
    oidcStorage = loadTokenStorage(ContextProvider.context)
    ssoTokenStorage = loadSSOTokenStorage(ContextProvider.context)
    cookiesStorage = loadCookiesStorage(ContextProvider.context)
}
kotlin

Preventing the Keystore System from using StrongBox

Devices running Android 9 or higher might be able to use a keystore system backed by StrongBox.

Storing keys, tokens, and secrets by using StrongBox offers the highest level of security for your app, and is the default option in the Ping SDK for Android.

However, using StrongBox can be slower, and more resource-intensive. When using StrongBox on certain devices the performance and responsiveness of your app may drop below acceptable levels. To learn more, refer to the device requirements for StrongBox in the Android Source documentation.

The Ping SDK for Android provides a strongBoxPreferred flag you can use to avoid the use of StrongBox if required. The flag only applies to the storage mechanisms built-in to the Ping SDK for Android. You do not have to provide your own custom storage to use the strongBoxPreferred flag.

If your app is using customized storage and you switch to using the built-in storage mechanisms the app will not be able to access the existing tokens and keys.

To avoid this, first call FRAuth.start with the original configuration and the customized storage, then call it a second time with the new store configuration and strongBoxPreferred flag.

Use the following code to use the built-in storage mechanisms prevent use of StrongBox:

Preventing use of StrongBox for storage
val myConfig = FROptionsBuilder.build {
  server {
    ...
  }
  oauth {
    ...
  }
  store {
    oidcStorage = TokenStorage(
      encryptor = EncryptorDelegate(
        SecretKeyEncryptor {
          context = <application context> (1)
          keyAlias = "<key alias>" (2)
          strongBoxPreferred = false (3)
        }
      )
    )
    ssoTokenStorage = SSOTokenStorage(
      encryptor = EncryptorDelegate(
        SecretKeyEncryptor {
          context = <application context> (1)
          keyAlias = "<key alias>" (2)
          strongBoxPreferred = false (3)
        }
      )
    )
    cookiesStorage = CookiesStorage(
      encryptor = EncryptorDelegate(
        SecretKeyEncryptor {
          context = <application context> (1)
          keyAlias = "<key alias>" (2)
          strongBoxPreferred = false (3)
        }
      )
    )
  }
}
kotlin
1 For <application context> enter the application context, such as ContextProvider.context.
2 For <key alias> enter a string used as the alias for the key the Ping SDK creates.

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.

3 To prevent use of StrongBox, set the strongBoxPreferred boolean to false.

If not specified or set to true, the Ping SDK for Android will use StrongBox when configured to use the built-in storage mechanisms.

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:

Conditionally applying flags based on device properties
store {
    if (Build.MANUFACTURER.contains("Example")) {
      oidcStorage = TokenStorage(
        encryptor = EncryptorDelegate(SecretKeyEncryptor {
          context = ContextProvider.context
          keyAlias = "com.example.v1.KEYS"
          strongBoxPreferred = false
        })
      )
    }
  }
kotlin

Customize storage on JavaScript

The Ping SDK for JavaScript provides two built-in storage schemes for OAuth 2.0 tokens:

Session storage

Store tokens using the sessionStorage API.

The browser clears session storage when a page session ends.

Local storage

Store tokens using the localStorage API.

The browser saves local storage data across browser sessions. This is the default setting, as it provides the highest browser compatibility.

You can configure your JavaScript apps to use customized storage if required.

Implement storage functions

You must implement the following functions in your custom storage scheme:

set(clientId, tokens)

Store a tokens object in the customized storage for a particular client.

get(clientId)

Retrieves the tokens object from the customized storage for a particular client.

remove(clientId)

Remove all items from the customized storage for a particular client.

Example:

let inMemoryTokens;

myTokenStore = {
  get(clientId) {
    console.log('Custom token getter used.');
    // Return a promise that resolves to any tokens stored in memory
    return Promise.resolve(inMemoryTokens);
  },
  set(clientId, tokens) {
    console.log('Custom token setter used.');
    // Example of storing tokens in memory
    inMemoryTokens = tokens;
    return Promise.resolve(undefined);
  },
  remove(clientId) {
    console.log('Custom token remover used.');
    // Reset the in-memory store
    inMemoryTokens = undefined;
    return Promise.resolve(undefined);
  },
};
typescript

Enable the custom storage

Use the tokenStore configuration property to configure the Ping SDK for JavaScript to use your custom storage object:

Config.setAsync({
    serverConfig: {
        wellknown: '/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
        timeout: 3000,
    },
    clientId: '',
    scope: '',
    redirectUri: `${window.location.origin}/callback.html`,
    tokenStore: myTokenStore
});
javascript