Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion android/src/main/java/com/frontegg/android/services/Api.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,11 @@ import java.io.IOException
open class Api(
private var credentialManager: CredentialManager
) {
private var httpClient: OkHttpClient = OkHttpClient()
private var httpClient: OkHttpClient = OkHttpClient.Builder()
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS) // For slow networks (EDGE)
.readTimeout(60, java.util.concurrent.TimeUnit.SECONDS) // For slow networks (EDGE)
.writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
private val storage = StorageProvider.getInnerStorage()
private val baseUrl: String
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ class FronteggAuthService(
MultiFactorAuthenticatorProvider.getMultiFactorAuthenticator()
private val stepUpAuthenticator =
StepUpAuthenticatorProvider.getStepUpAuthenticator(credentialManager)

@Volatile
private var refreshingInProgress = false

override val isMultiRegion: Boolean
get() = regions.isNotEmpty()
Expand Down Expand Up @@ -217,18 +214,42 @@ class FronteggAuthService(
}
}

@Volatile
private var refreshingInProgress = false
private var refreshRetryCount = 0
private val maxRetries = 2
private val baseRetryDelayMs = 3000L

override fun refreshTokenIfNeeded(): Boolean {
return try {
refreshingInProgress = true
this.sendRefreshToken()
val success = this.sendRefreshToken()
if (success) {
refreshRetryCount = 0 // Reset on success
}
success
} catch (e: FailedToAuthenticateException) {
Log.e(TAG, "Refresh token is invalid, clearing credentials", e)
refreshRetryCount = 0
// Clear credentials when refresh token is invalid
clearCredentials()
false
} catch (e: Exception) {
Log.e(TAG, "Failed to send refresh token request", e)
false
Log.e(TAG, "Failed to send refresh token request (attempt ${refreshRetryCount + 1})", e)

// Retry on network errors only
if (isNetworkError() && refreshRetryCount < maxRetries) {
refreshRetryCount++
val delayMs = baseRetryDelayMs * refreshRetryCount // 3s, 6s
Log.d(TAG, "Scheduling retry in ${delayMs}ms (attempt ${refreshRetryCount}/$maxRetries)")

// Schedule retry using the timer
refreshTokenTimer.scheduleTimer(delayMs)
return false
} else {
refreshRetryCount = 0
return false
}
} finally {
refreshingInProgress = false
}
Expand Down Expand Up @@ -521,11 +542,19 @@ class FronteggAuthService(
refreshToken.value = refreshTokenSaved

if (!refreshTokenIfNeeded()) {
// Offline-safe: keep saved refresh token to allow later reconnect
accessToken.value = null
// keep refreshToken.value as is
// Check if this is a network error or auth error
if (isNetworkError()) {
// Offline-safe: keep saved refresh token to allow later reconnect
// But mark as not authenticated until we can refresh
accessToken.value = null
isAuthenticated.value = false
isLoading.value = true // Keep loading state for retry
// keep refreshToken.value as is
} else {
// Auth error: clear tokens as refresh token is invalid
clearCredentials()
}
initializing.value = false
isLoading.value = false
}

} else {
Expand All @@ -548,8 +577,41 @@ class FronteggAuthService(
try {
val data = api.refreshToken(refreshToken)
lastRefreshError = null // Clear error on success
val success = setCredentials(data.access_token, data.refresh_token)
return success

// Save new tokens immediately to prevent rotation issues
credentialManager.save(CredentialKeys.REFRESH_TOKEN, data.refresh_token)
credentialManager.save(CredentialKeys.ACCESS_TOKEN, data.access_token)

// Update state with new tokens
this.refreshToken.value = data.refresh_token
this.accessToken.value = data.access_token

// Try to get user info, but don't fail if network is slow
try {
val user = api.me()
if (user != null) {
this.user.value = user
this.isAuthenticated.value = true

// Schedule next refresh timer
refreshTokenTimer.cancelLastTimer()
val decoded = JWTHelper.decode(data.access_token)
if (decoded.exp > 0) {
val offset = decoded.exp.calculateTimerOffset()
refreshTokenTimer.scheduleTimer(offset)
}
}
} catch (e: Exception) {
// If me() fails due to network, keep tokens but mark as loading
Log.w(TAG, "Failed to fetch user info after token refresh, keeping tokens", e)
this.isLoading.value = true
}

// Always set initializing to false after successful token refresh
this.initializing.value = false
this.isLoading.value = false

return true
} catch (e: Exception) {
lastRefreshError = e
throw e
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,20 +78,22 @@ object FronteggReconnector {
delay(50)
}
} catch (t: Throwable) {
Log.e(TAG, "Failed to handle network reconnect", t)
Log.e(TAG, "Failed to retry SDK initialization", t)
}

// Always try to refresh if possible, swallow network errors
try {
try {
if (FronteggApp.instance != null) {
Log.d(TAG, "Attempting token refresh on network reconnect")
val refreshSuccess = context.fronteggAuth.refreshTokenIfNeeded()
if (refreshSuccess) {
Log.d(TAG, "Token refresh successful on network reconnect")
} else {
Log.d(TAG, "Token refresh failed on network reconnect, will retry later")
}
} catch (_: Throwable) {
// If FronteggApp not initialized, skip refresh
}
} catch (t: Throwable) {
Log.e(TAG, "Failed to handle network reconnect", t)
Log.e(TAG, "Failed to refresh token on network reconnect", t)
}
}
}
Expand Down
Loading