Store JWT Token with Coroutines

charlesetieve

Charles Etieve

Posted on April 5, 2023

Store JWT Token with Coroutines

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] }
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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.

Meme about adroid developer choosing between coroutines and encryption for JWT Token

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")
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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! 😊

💖 💪 🙅 🚩
charlesetieve
Charles Etieve

Posted on April 5, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related