You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: samples/android/manifest.yaml
+1-1Lines changed: 1 addition & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -29,7 +29,7 @@ scenarios:
29
29
app_type: mobile_desktop
30
30
display_name: "Token Refresh"
31
31
grant: "Refresh Token"
32
-
description: "Use refresh tokens to renew access tokens without re-prompting. Requires offline_access. Refresh tokens are stored in Android EncryptedSharedPreferences (Keystore-backed)."
32
+
description: "Use refresh tokens to renew access tokens without re-prompting. Requires offline_access. Refresh tokens are stored in platform secure storage (Keychain / EncryptedSharedPreferences)."
33
33
callout: "Requires refresh_token grant type enabled in workspace [OAuth settings](workspace-oauth-general) and in the application's [Grant Types](workspace-applications-oauth). Also requires the offline_access scope."
@@ -202,6 +207,7 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) {
202
207
if (_tokens.value !=null) return
203
208
val stored = store.load()?.takeIf { it.isNotEmpty() } ?:return
204
209
val issuerJavaUri =AuthConfig.issuer ?:return
210
+
if (AuthConfig.clientId.isEmpty()) return
205
211
try {
206
212
val config = discoverConfiguration(Uri.parse(issuerJavaUri.toString()))
207
213
val request =TokenRequest.Builder(config, AuthConfig.clientId)
@@ -214,10 +220,10 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) {
214
220
val next = toTokens(response, fallback =null)
215
221
_tokens.value = next
216
222
if (next.refreshToken !=null&& next.refreshToken != stored) {
217
-
store.save(next.refreshToken)
223
+
persistRefreshToken(next.refreshToken)
218
224
}
219
225
scheduleRefreshTimer()
220
-
} catch (_:Throwable) {
226
+
} catch (_:Exception) {
221
227
// Stored token is no longer valid — let the user sign in.
222
228
store.clear()
223
229
}
@@ -242,6 +248,18 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) {
242
248
}
243
249
}
244
250
251
+
/** Save the refresh token to EncryptedSharedPreferences. If the write fails (e.g.,
252
+
* Keystore unavailable), surface a warning so the user knows the README's silent
253
+
* re-login promise won't hold on next launch — but keep the in-memory token so
254
+
* the current session still refreshes the access token. */
255
+
privatefunpersistRefreshToken(refresh:String) {
256
+
try {
257
+
store.save(refresh)
258
+
} catch (e:Exception) {
259
+
_error.value ="Sign-in succeeded but refresh token could not be saved to secure storage (${e.localizedMessage}). Silent re-login on next launch will not work."
260
+
}
261
+
}
262
+
245
263
/** Project an AppAuth TokenResponse onto our flat Tokens data class. Refresh
246
264
* responses sometimes omit a fresh refresh_token or id_token — when missing,
247
265
* fall back to the previously-known values so display state stays populated. */
@@ -283,13 +301,16 @@ class AuthViewModel(app: Application) : AndroidViewModel(app) {
283
301
val conn = (URL(revokeUrl).openConnection() asHttpURLConnection).apply {
Copy file name to clipboardExpand all lines: snippets.json
+4-4Lines changed: 4 additions & 4 deletions
Original file line number
Diff line number
Diff line change
@@ -155,10 +155,10 @@
155
155
{
156
156
"step": 3,
157
157
"description": "Trigger login with offline_access and capture the refresh token",
158
-
"code": " suspend fun signIn(): Intent? {\n _error.value = null\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return null }\n if (AuthConfig.clientId.isEmpty()) {\n _error.value = \"CIAM_CLIENT_ID is missing — fill in local.properties\"; return null\n }\n val redirect = AuthConfig.redirectUri\n ?: run { _error.value = \"CIAM_REDIRECT_SCHEME is missing — fill in local.properties\"; return null }\n return try {\n val issuerAndroidUri = Uri.parse(issuerJavaUri.toString())\n val config = discoverConfiguration(issuerAndroidUri)\n val request = AuthorizationRequest.Builder(\n config,\n AuthConfig.clientId,\n ResponseTypeValues.CODE,\n redirect,\n )\n .setScopes(AuthConfig.scopes)\n .build()\n pendingAuthState = AuthState(config)\n authService.getAuthorizationRequestIntent(request)\n } catch (e: Throwable) {\n _error.value = e.localizedMessage ?: \"Authorization failed\"\n null\n }\n }",
158
+
"code": " suspend fun signIn(): Intent? {\n _error.value = null\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return null }\n if (AuthConfig.clientId.isEmpty()) {\n _error.value = \"CIAM_CLIENT_ID is missing — fill in local.properties\"; return null\n }\n val redirect = AuthConfig.redirectUri\n ?: run { _error.value = \"CIAM_REDIRECT_SCHEME is missing — fill in local.properties\"; return null }\n return try {\n val issuerAndroidUri = Uri.parse(issuerJavaUri.toString())\n val config = discoverConfiguration(issuerAndroidUri)\n val request = AuthorizationRequest.Builder(\n config,\n AuthConfig.clientId,\n ResponseTypeValues.CODE,\n redirect,\n )\n .setScopes(AuthConfig.scopes)\n .build()\n // Assign pendingAuthState only after the Intent is successfully created so\n // a thrown getAuthorizationRequestIntent doesn't leave stale state behind.\n // Catch Exception (not Throwable) so coroutine CancellationException still\n // propagates and structured concurrency works correctly.\n val intent = authService.getAuthorizationRequestIntent(request)\n pendingAuthState = AuthState(config)\n intent\n } catch (e: Exception) {\n _error.value = e.localizedMessage ?: \"Authorization failed\"\n null\n }\n }",
"description": "Exchange the refresh token for a new access token; on failure clear local state and require re-login",
174
-
"code": " suspend fun refreshTokens() {\n _error.value = null\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return }\n val storedRefresh = _tokens.value?.refreshToken?.takeIf { it.isNotEmpty() }\n ?: store.load()\n ?: run { _error.value = \"No refresh token available. Sign in first.\"; return }\n try {\n val config = discoverConfiguration(Uri.parse(issuerJavaUri.toString()))\n val request = TokenRequest.Builder(config, AuthConfig.clientId)\n .setGrantType(GrantTypeValues.REFRESH_TOKEN)\n .setRefreshToken(storedRefresh)\n .build()\n val response = performTokenRequest(request)\n val next = toTokens(response, fallback = _tokens.value)\n _tokens.value = next\n // If the IdP rotated the refresh token, persist the new one.\n if (next.refreshToken != null && next.refreshToken != storedRefresh) {\n store.save(next.refreshToken)\n }\n scheduleRefreshTimer()\n } catch (e: Throwable) {\n // Refresh tokens can be revoked or expire — clear local state and force re-login.\n store.clear()\n _tokens.value = null\n refreshJob?.cancel()\n _error.value = \"Refresh failed (${e.localizedMessage}). Sign in again.\"\n }\n }",
174
+
"code": " suspend fun refreshTokens() {\n _error.value = null\n val issuerJavaUri = AuthConfig.issuer\n ?: run { _error.value = \"CIAM_ISSUER_HOST is missing — fill in local.properties\"; return }\n if (AuthConfig.clientId.isEmpty()) {\n _error.value = \"CIAM_CLIENT_ID is missing — fill in local.properties\"; return\n }\n val storedRefresh = _tokens.value?.refreshToken?.takeIf { it.isNotEmpty() }\n ?: store.load()\n ?: run { _error.value = \"No refresh token available. Sign in first.\"; return }\n try {\n val config = discoverConfiguration(Uri.parse(issuerJavaUri.toString()))\n val request = TokenRequest.Builder(config, AuthConfig.clientId)\n .setGrantType(GrantTypeValues.REFRESH_TOKEN)\n .setRefreshToken(storedRefresh)\n .build()\n val response = performTokenRequest(request)\n val next = toTokens(response, fallback = _tokens.value)\n _tokens.value = next\n // If the IdP rotated the refresh token, persist the new one.\n if (next.refreshToken != null && next.refreshToken != storedRefresh) {\n persistRefreshToken(next.refreshToken)\n }\n scheduleRefreshTimer()\n } catch (e: Exception) {\n // Refresh tokens can be revoked or expire — clear local state and force re-login.\n store.clear()\n _tokens.value = null\n refreshJob?.cancel()\n _error.value = \"Refresh failed (${e.localizedMessage}). Sign in again.\"\n }\n }",
0 commit comments