Customize storage
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:
-
OAuth 2.0 / OpenID Connect 1.0 tokens
-
SSO data
-
Cookies
Depending on why you want to override storage mechanisms, you might prefer instead to prevent use of StrongBox. Learn more in Preventing the Keystore System from using 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:
-
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. 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 |
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 = "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 You cannot add the parameters to the |
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)
}
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 |
Use the following code to use the built-in storage mechanisms prevent use of StrongBox:
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)
}
)
)
}
}
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 For example, |
3 | To prevent use of StrongBox, set the strongBoxPreferred boolean to false .
If not specified or set to |
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:
store {
if (Build.MANUFACTURER.contains("Example")) {
oidcStorage = TokenStorage(
encryptor = EncryptorDelegate(SecretKeyEncryptor {
context = ContextProvider.context
keyAlias = "com.example.v1.KEYS"
strongBoxPreferred = false
})
)
}
}
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);
},
};
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: 'https://openam-forgerock-sdks.forgeblocks.com/am/oauth2/realms/root/realms/alpha/.well-known/openid-configuration',
timeout: 3000,
},
clientId: 'sdkPublicClient',
scope: 'openid profile email address',
redirectUri: `${window.location.origin}/callback.html`,
tokenStore: myTokenStore
});