JWT (JSON Web Token) has become a popular standard for implementing stateless authentication in modern mobile apps. In order to maintain security and avoid session hijacking, it's important to store JWT tokens securely on the client-side.
DataStore is a popular choice for storing tokens as it provides the advantages of shared preferences along with additional coroutines capabilities:
// store token (must be called from suspend function)
dataStore.edit { it[KEY_TOKEN] = token }
// read token (returns a flow of token)
dataStore.data.map { it[KEY_TOKEN] }
One powerful use case is to navigate to the home or login view on wether the token exists or not:
dataStore.data
.map { it.contains(KEY_TOKEN) }
.onEach { isAuthenticated ->
when(isAuthenticated) {
true -> navController.navigate("home")
false -> navController.navigate("login")
}
}.launchIn(scope)
However DataStore lacks support for encryption, whereas shared preferences does. This means developers have to choose between the convenience of coroutines and the security of encryption when choosing a method to store their JWT tokens.
Fortunately, there is a library Encrypted DataStore that extends DataStore and allows for easy encryption. While this solution is currently useful, it may become deprecated in the future once DataStore natively implements this functionality.
Enough talk, let's dive into the code.
First, import libraries in your build.gradle (app module):
// datastore
implementation("androidx.datastore:datastore-preferences:1.0.0")
// extension for datastore to support encryption
implementation("io.github.osipxd:security-crypto-datastore-preferences:1.0.0-alpha04")
// utility library for datastore encryption
implementation("androidx.security:security-crypto-ktx:1.1.0-alpha05")
Then you will need to build your datastore to inject it in your AuthenticationService:
val dataStore = PreferenceDataStoreFactory.createEncrypted{
EncryptedFile.Builder(
context,
// the file should have extension .preferences_pb
dataStoreFile("filename.preferences_pb"),
MasterKey
.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build(),
EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()
}
Finally you can add this service to your project:
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.map
class AuthenticationService(
private val dataStore: DataStore<Preferences>
) {
// returns a flow of is authenticated state
fun isAuthenticated(): Flow<Boolean> {
// flow of token existence from dataStore
return dataStore.data.map {
it.contains(KEY_TOKEN)
}
}
// store new token after sign in or token refresh
suspend fun store(token: String) {
// store token to dataStore
dataStore.edit {
it[KEY_TOKEN] = token
}
}
// get token for protected API method
suspend fun getToken(): String {
return dataStore.data
.map { it[KEY_TOKEN] } // get a flow of token from dataStore
.firstOrNull() // transform flow to suspend
?: throw IllegalArgumentException("no token stored")
}
// to call when user logs out or when refreshing the token has failed
suspend fun onLogout() {
// remove token from dataStore
dataStore.edit {
it.remove(KEY_TOKEN)
}
}
companion object {
val KEY_TOKEN = stringPreferencesKey("key_token")
}
}
In conclusion, there is now an efficient and secure data storage option available for your JWT tokens, so there's no excuse to settle for anything less! π
Top comments (0)