Skip to content

Commit 5fcdbf0

Browse files
authored
Merge pull request #259 from FusionAuth/miker/issue-184
DataStoreStorage replaces the deprecated SharedPreferencesStorage
2 parents 477958e + c3acbad commit 5fcdbf0

File tree

12 files changed

+159
-76
lines changed

12 files changed

+159
-76
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ The `fusionauth_config.json` file should be placed in the `res/raw` directory an
101101

102102
By default, the SDK uses the `MemoryStorage` for storing tokens. This means that tokens will be lost when the app is
103103
closed.
104-
To persist tokens, you can use the `SharedPreferencesStorage` or implement your own `TokenStorage`.
104+
To persist tokens, you can use the `DataStoreStorage` or implement your own `TokenStorage`.
105105
<!--
106106
end::forDocSiteGettingStarted[]
107107
-->

app/src/androidTest/java/io/fusionauth/sdk/FullEnd2EndTest.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import androidx.test.uiautomator.By
1515
import androidx.test.uiautomator.UiDevice
1616
import androidx.test.uiautomator.Until
1717
import io.fusionauth.mobilesdk.AuthorizationManager
18+
import kotlinx.coroutines.runBlocking
1819
import org.junit.After
1920
import org.junit.Before
2021
import org.junit.Rule
@@ -85,13 +86,13 @@ internal class FullEnd2EndTest {
8586
logger.info("Token activity displayed")
8687

8788
// Check refresh token functionality
88-
val expirationTime = AuthorizationManager.getAccessTokenExpirationTime()!!
89+
val expirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
8990
logger.info("Check refresh token")
9091
onView(withId(R.id.refresh_token))
9192
.check(matches(isDisplayed()))
9293
.perform(click())
9394

94-
val newExpirationTime = AuthorizationManager.getAccessTokenExpirationTime()!!
95+
val newExpirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
9596

9697
logger.info("Token was refreshed (${expirationTime} to ${newExpirationTime})")
9798
check(newExpirationTime > expirationTime) { "Token was not refreshed" }
@@ -193,13 +194,13 @@ internal class FullEnd2EndTest {
193194
logger.info("Token activity displayed for user in reset configuration tenant")
194195

195196
// Check refresh token functionality
196-
val expirationTime = AuthorizationManager.getAccessTokenExpirationTime()!!
197+
val expirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
197198
logger.info("Check refresh token")
198199
onView(withId(R.id.refresh_token))
199200
.check(matches(isDisplayed()))
200201
.perform(click())
201202

202-
val newExpirationTime = AuthorizationManager.getAccessTokenExpirationTime()!!
203+
val newExpirationTime = runBlocking { AuthorizationManager.getAccessTokenExpirationTime()!! }
203204

204205
logger.info("Token was refreshed (${expirationTime} to ${newExpirationTime})")
205206
check(newExpirationTime > expirationTime) { "Token was not refreshed" }

app/src/main/java/io/fusionauth/sdk/LoginActivity.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import io.fusionauth.mobilesdk.AuthorizationConfiguration
2626
import io.fusionauth.mobilesdk.AuthorizationManager
2727
import io.fusionauth.mobilesdk.oauth.OAuthAuthorizeOptions
2828
import io.fusionauth.mobilesdk.exceptions.AuthorizationException
29-
import io.fusionauth.mobilesdk.storage.SharedPreferencesStorage
29+
import io.fusionauth.mobilesdk.storage.DataStoreStorage
3030
import kotlinx.coroutines.launch
3131

3232
/**
@@ -41,15 +41,17 @@ class LoginActivity : AppCompatActivity() {
4141
if (!AuthorizationManager.isInitialized()) {
4242
AuthorizationManager.initialize(
4343
AuthorizationConfiguration.fromResources(this, R.raw.fusionauth_config),
44-
SharedPreferencesStorage(this)
44+
DataStoreStorage(this)
4545
)
4646
}
4747

48-
if (AuthorizationManager.isAuthenticated()) {
49-
Log.i(TAG, "User is already authenticated, proceeding to token activity")
50-
startActivity(Intent(this, TokenActivity::class.java))
51-
finish()
52-
return
48+
lifecycleScope.launch {
49+
if (AuthorizationManager.isAuthenticated()) {
50+
Log.i(TAG, "User is already authenticated, proceeding to token activity")
51+
startActivity(Intent(this@LoginActivity, TokenActivity::class.java))
52+
finish()
53+
return@launch
54+
}
5355
}
5456

5557
setContentView(R.layout.activity_login)

app/src/main/java/io/fusionauth/sdk/TokenActivity.kt

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import io.fusionauth.mobilesdk.AuthorizationManager
3030
import io.fusionauth.mobilesdk.FusionAuthState
3131
import io.fusionauth.mobilesdk.UserInfo
3232
import io.fusionauth.mobilesdk.exceptions.AuthorizationException
33-
import io.fusionauth.mobilesdk.storage.SharedPreferencesStorage
33+
import io.fusionauth.mobilesdk.storage.DataStoreStorage
3434
import kotlinx.coroutines.launch
3535
import org.json.JSONException
3636
import java.io.IOException
@@ -58,7 +58,7 @@ class TokenActivity : AppCompatActivity() {
5858
if (!AuthorizationManager.isInitialized()) {
5959
AuthorizationManager.initialize(
6060
AuthorizationConfiguration.fromResources(this, R.raw.fusionauth_config),
61-
SharedPreferencesStorage(this)
61+
DataStoreStorage(this)
6262
)
6363
}
6464

@@ -75,21 +75,20 @@ class TokenActivity : AppCompatActivity() {
7575
}
7676

7777
Logger.getLogger(TAG).info("Checking for authorization response")
78-
if (AuthorizationManager.isAuthenticated()) {
79-
fetchUserInfoAndDisplayAuthorized(/*authState.getAccessToken()*/)
80-
return
81-
}
82-
8378
lifecycleScope.launch {
84-
displayLoading("Exchanging authorization code")
85-
try {
86-
val authState: FusionAuthState = AuthorizationManager.oAuth(this@TokenActivity)
87-
.handleRedirect(intent)
88-
Log.i(TAG, authState.toString())
79+
if (AuthorizationManager.isAuthenticated()) {
8980
fetchUserInfoAndDisplayAuthorized()
90-
} catch (ex: AuthorizationException) {
91-
Log.e(TAG, "Failed to exchange authorization code", ex)
92-
displayNotAuthorized("Authorization failed")
81+
} else {
82+
displayLoading("Exchanging authorization code")
83+
try {
84+
val authState: FusionAuthState = AuthorizationManager.oAuth(this@TokenActivity)
85+
.handleRedirect(intent)
86+
Log.i(TAG, authState.toString())
87+
fetchUserInfoAndDisplayAuthorized()
88+
} catch (ex: AuthorizationException) {
89+
Log.e(TAG, "Failed to exchange authorization code", ex)
90+
displayNotAuthorized("Authorization failed")
91+
}
9392
}
9493
}
9594
}
@@ -129,7 +128,7 @@ class TokenActivity : AppCompatActivity() {
129128
}
130129

131130
@MainThread
132-
private fun displayAuthorized() {
131+
private suspend fun displayAuthorized() {
133132
findViewById<View>(R.id.authorized).visibility = View.VISIBLE
134133
findViewById<View>(R.id.not_authorized).visibility = View.GONE
135134
findViewById<View>(R.id.loading_container).visibility = View.GONE
@@ -214,7 +213,7 @@ class TokenActivity : AppCompatActivity() {
214213
showSnackbar("Failed to parse user info")
215214
}
216215

217-
runOnUiThread { this@TokenActivity.displayAuthorized() }
216+
this@TokenActivity.displayAuthorized()
218217
}
219218
}
220219

@@ -265,12 +264,14 @@ class TokenActivity : AppCompatActivity() {
265264

266265
@MainThread
267266
private fun signOut() {
268-
AuthorizationManager.clearState()
267+
lifecycleScope.launch {
268+
AuthorizationManager.clearState()
269269

270-
val mainIntent = Intent(this, LoginActivity::class.java)
271-
mainIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
272-
startActivity(mainIntent)
273-
finish()
270+
val mainIntent = Intent(this@TokenActivity, LoginActivity::class.java)
271+
mainIntent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP
272+
startActivity(mainIntent)
273+
finish()
274+
}
274275
}
275276

276277
@MainThread

library/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ dependencies {
176176
implementation("androidx.security:security-crypto-ktx:1.1.0")
177177
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
178178
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2")
179+
implementation("androidx.datastore:datastore-preferences:1.2.0")
179180
testImplementation("junit:junit:4.13.2")
180181
testImplementation("org.mockito:mockito-core:5.21.0")
181182
testImplementation("org.mockito.kotlin:mockito-kotlin:6.1.0")

library/src/main/java/io/fusionauth/mobilesdk/AuthorizationManager.kt

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ object AuthorizationManager {
6161
*
6262
* @return true if the user is authenticated, false otherwise
6363
*/
64-
fun isAuthenticated(): Boolean {
64+
suspend fun isAuthenticated(): Boolean {
6565
return !isAccessTokenExpired()
6666
}
6767

@@ -107,7 +107,7 @@ object AuthorizationManager {
107107
*
108108
* @return The access token string or null if not available.
109109
*/
110-
fun getAccessToken(): String? {
110+
suspend fun getAccessToken(): String? {
111111
return tokenManager.getAuthState()?.accessToken
112112
}
113113

@@ -117,7 +117,7 @@ object AuthorizationManager {
117117
* @return The expiration time of the access token, or null if the token manager is not set or the access token is
118118
* not available.
119119
*/
120-
fun getAccessTokenExpirationTime(): Long? {
120+
suspend fun getAccessTokenExpirationTime(): Long? {
121121
return tokenManager.getAuthState()?.accessTokenExpirationTime
122122
}
123123

@@ -126,7 +126,7 @@ object AuthorizationManager {
126126
*
127127
* @return true if the access token is expired, false otherwise.
128128
*/
129-
fun isAccessTokenExpired(): Boolean {
129+
suspend fun isAccessTokenExpired(): Boolean {
130130
return getAccessTokenExpirationTime()?.let {
131131
it < System.currentTimeMillis()
132132
} ?: true
@@ -137,7 +137,7 @@ object AuthorizationManager {
137137
*
138138
* @return The ID token string, or null if the user is not authenticated.
139139
*/
140-
fun getIdToken(): String? {
140+
suspend fun getIdToken(): String? {
141141
return tokenManager.getAuthState()?.idToken
142142
}
143143

@@ -154,7 +154,7 @@ object AuthorizationManager {
154154
* @return The parsed ID token, or null if it cannot be parsed.
155155
*/
156156
@OptIn(ExperimentalEncodingApi::class)
157-
fun getParsedIdToken(): IdToken? {
157+
suspend fun getParsedIdToken(): IdToken? {
158158
return tokenManager.getAuthState()?.idToken?.let {
159159
val parts = it.split(".")
160160
require(parts.size == JWT_PARTS) { "Invalid JWT token" }
@@ -167,7 +167,7 @@ object AuthorizationManager {
167167
*
168168
* This method clears the authorization state by removing the "authState" key from the storage.
169169
*/
170-
fun clearState() {
170+
suspend fun clearState() {
171171
tokenManager.clearAuthState()
172172
}
173173

library/src/main/java/io/fusionauth/mobilesdk/TokenManager.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class TokenManager {
3939
* @throws StorageException if an error occurs while decoding the authorization state.
4040
*/
4141
@Suppress("TooGenericExceptionCaught")
42-
fun getAuthState(): FusionAuthState? {
42+
suspend fun getAuthState(): FusionAuthState? {
4343
if (this.authState.get() != null) {
4444
return this.authState.get()
4545
}
@@ -62,7 +62,7 @@ class TokenManager {
6262
* @param authState The authorization state to be saved.
6363
* @throws NullPointerException if `storage` is null.
6464
*/
65-
fun saveAuthState(authState: FusionAuthState) {
65+
suspend fun saveAuthState(authState: FusionAuthState) {
6666
if (this.storage == null) throw StorageException.notSet()
6767

6868
this.authState.set(authState)
@@ -74,7 +74,7 @@ class TokenManager {
7474
*
7575
* @throws StorageException if the storage implementation is not set.
7676
*/
77-
fun clearAuthState() {
77+
suspend fun clearAuthState() {
7878
if (this.storage == null) throw StorageException.notSet()
7979

8080
this.authState.set(null)

library/src/main/java/io/fusionauth/mobilesdk/oauth/OAuthAuthorizationService.kt

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -477,44 +477,41 @@ class OAuthAuthorizationService internal constructor(
477477
*/
478478
private suspend fun freshAccessTokenInternal(): String? {
479479
val config = getConfiguration()
480+
val authService = getAuthorizationService()
480481

481-
return suspendCoroutine {
482-
val authService = getAuthorizationService()
482+
val authState = tokenManager?.getAuthState()
483+
?: throw AuthorizationException("Not authenticated.")
483484

484-
val refreshToken = tokenManager?.getAuthState()?.refreshToken
485-
if (refreshToken == null) {
486-
it.resumeWithException(AuthorizationException("No refresh token available"))
487-
return@suspendCoroutine
488-
}
485+
val refreshToken = authState.refreshToken
486+
?: throw AuthorizationException("No refresh token available")
489487

488+
val response = suspendCoroutine<TokenResponse> { continuation ->
490489
authService.performTokenRequest(
491-
TokenRequest.Builder(
492-
config,
493-
clientId
494-
)
490+
TokenRequest.Builder(config, clientId)
495491
.setGrantType(GrantTypeValues.REFRESH_TOKEN)
496492
.setRefreshToken(refreshToken)
497493
.build(),
498494
appAuthState.clientAuthentication
499495
) { response, exception ->
496+
appAuthState.update(response, exception)
500497
if (response != null) {
501-
val authState = tokenManager?.getAuthState()
502-
if (authState != null) {
503-
val newAuthState = authState.copy(
504-
accessToken = response.accessToken,
505-
accessTokenExpirationTime = response.accessTokenExpirationTime,
506-
idToken = response.idToken,
507-
refreshToken = response.refreshToken,
508-
)
509-
tokenManager?.saveAuthState(newAuthState)
510-
}
511-
it.resume(response.accessToken)
498+
continuation.resume(response)
512499
} else {
513-
it.resumeWithException(exception?.let { ex -> AuthorizationException(ex) }
500+
continuation.resumeWithException(exception?.let { AuthorizationException(it) }
514501
?: AuthorizationException("Unknown error"))
515502
}
516503
}
517504
}
505+
506+
val newAuthState = authState.copy(
507+
accessToken = response.accessToken,
508+
accessTokenExpirationTime = response.accessTokenExpirationTime,
509+
idToken = response.idToken,
510+
refreshToken = response.refreshToken ?: authState.refreshToken,
511+
)
512+
tokenManager.saveAuthState(newAuthState)
513+
514+
return response.accessToken
518515
}
519516

520517
/**

0 commit comments

Comments
 (0)