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

Configure storage overrides

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

val options = FROptionsBuilder.build {
    server {
       url = "https://openam-forgerock-sdks.forgeblocks.com/am"
       realm = "alpha"
       cookieName = "iPlanetDirectoryPro"
    }
    oauth {
       oauthClientId = "sdkPublicClient"
       oauthRedirectUri = "https://localhost:8443/callback"
       oauthScope = "openid profile email address"
    }
    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);

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

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:

  • OpenID Connect storage

  • SSO token storage

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")
    }
}
class MyCustomSSOTokenStorage(context: Context) : Storage<SSOToken> {

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

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

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

}

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.

This may mean your users 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

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()
    }
}
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)
}