Skip to content

Commit c3c46ff

Browse files
committed
Fix Login State Loss and OAuth Flow Interruption
This commit addresses a critical issue where the application would lose its authentication state and reset to the login screen when the user switched applications during the OAuth flow (e.g., to retrieve credentials from a password manager) or when the OS killed the background process. Key Changes: 1. **Manifest Configuration**: - Updated LoginActivity launchMode from singleTask to singleTop. This prevents the activity from being unnecessarily destroyed and recreated when receiving the OAuth redirect intent, preserving the activity stack. 2. **ViewModel Updates (AuthenticationViewModel.kt)**: - Changed PKCE state properties (codeVerifier, codeChallenge, oidcState) from immutable �al to mutable �ar. This allows these values to be restored after process death, which is essential for completing the OAuth handshake. 3. **State Persistence (LoginActivity.kt)**: - Implemented saveAuthState() and estoreAuthState() methods using SharedPreferences. This ensures that the PKCE parameters survive process death and activity recreation, which savedInstanceState alone was not reliably handling for this specific flow. - Added clearAuthState() to remove temporary authentication data upon successful login or error, ensuring security and cleanliness. - Integrated these methods into the lifecycle: saving state before launching the browser and restoring it in onCreate if needed. 4. **OAuth Redirect Handling**: - Added 'trampoline' logic in onCreate to correctly handle OAuth redirect intents. If the activity is not the task root, it relaunches itself with CLEAR_TOP and SINGLE_TOP flags to bring the existing instance to the front without losing state. - Modified handleDeepLink to explicitly ignore OAuth redirect intents (checking for 'code' and 'error' parameters), preventing them from being misidentified as regular deep links. This comprehensive fix ensures a robust login experience even on devices with aggressive background process killing or when users multitask during authentication.
1 parent 252eb16 commit c3c46ff

3 files changed

Lines changed: 53 additions & 5 deletions

File tree

opencloudApp/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@
236236
android:name=".presentation.authentication.LoginActivity"
237237
android:exported="true"
238238
android:label="@string/login_label"
239-
android:launchMode="singleTask"
239+
android:launchMode="singleTop"
240240
android:theme="@style/Theme.openCloud.Toolbar">
241241
<intent-filter>
242242
<action android:name="android.intent.action.VIEW" />

opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/AuthenticationViewModel.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ class AuthenticationViewModel(
7272
private val contextProvider: ContextProvider,
7373
) : ViewModel() {
7474

75-
val codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
76-
val codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
77-
val oidcState: String = OAuthUtils().generateRandomState()
75+
var codeVerifier: String = OAuthUtils().generateRandomCodeVerifier()
76+
var codeChallenge: String = OAuthUtils().generateCodeChallenge(codeVerifier)
77+
var oidcState: String = OAuthUtils().generateRandomState()
7878

7979
private val _legacyWebfingerHost = MediatorLiveData<Event<UIResult<String>>>()
8080
val legacyWebfingerHost: LiveData<Event<UIResult<String>>> = _legacyWebfingerHost

opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication/LoginActivity.kt

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ import java.io.File
9696

9797
private const val KEY_SERVER_BASE_URL = "KEY_SERVER_BASE_URL"
9898
private const val KEY_OIDC_SUPPORTED = "KEY_OIDC_SUPPORTED"
99+
private const val KEY_CODE_VERIFIER = "KEY_CODE_VERIFIER"
100+
private const val KEY_CODE_CHALLENGE = "KEY_CODE_CHALLENGE"
101+
private const val KEY_OIDC_STATE = "KEY_OIDC_STATE"
99102

100103
class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced {
101104

@@ -118,6 +121,16 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
118121
private var resultBundle: Bundle? = null
119122

120123
override fun onCreate(savedInstanceState: Bundle?) {
124+
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
125+
if (!isTaskRoot) {
126+
val newIntent = Intent(this, LoginActivity::class.java)
127+
newIntent.data = intent.data
128+
newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
129+
startActivity(newIntent)
130+
finish()
131+
return
132+
}
133+
}
121134
super.onCreate(savedInstanceState)
122135

123136
checkPasscodeEnforced(this)
@@ -142,6 +155,9 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
142155
authTokenType = savedInstanceState.getString(KEY_AUTH_TOKEN_TYPE)
143156
savedInstanceState.getString(KEY_SERVER_BASE_URL)?.let { serverBaseUrl = it }
144157
oidcSupported = savedInstanceState.getBoolean(KEY_OIDC_SUPPORTED)
158+
savedInstanceState.getString(KEY_CODE_VERIFIER)?.let { authenticationViewModel.codeVerifier = it }
159+
savedInstanceState.getString(KEY_CODE_CHALLENGE)?.let { authenticationViewModel.codeChallenge = it }
160+
savedInstanceState.getString(KEY_OIDC_STATE)?.let { authenticationViewModel.oidcState = it }
145161
}
146162

147163
// UI initialization
@@ -211,10 +227,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
211227
accountAuthenticatorResponse?.onRequestContinued()
212228

213229
initLiveDataObservers()
230+
231+
if (intent.data != null && (intent.data?.getQueryParameter("code") != null || intent.data?.getQueryParameter("error") != null)) {
232+
if (savedInstanceState == null) {
233+
restoreAuthState()
234+
}
235+
handleGetAuthorizationCodeResponse(intent)
236+
}
214237
}
215238

216239
private fun handleDeepLink() {
217-
if (intent.data != null) {
240+
if (intent.data != null && intent.data?.getQueryParameter("code") == null && intent.data?.getQueryParameter("error") == null) {
218241
authenticationViewModel.launchedFromDeepLink = true
219242
if (getAccounts(baseContext).isNotEmpty()) {
220243
launchFileDisplayActivity()
@@ -486,6 +509,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
486509
setResult(Activity.RESULT_OK, intent)
487510

488511
authenticationViewModel.discoverAccount(accountName = accountName, discoveryNeeded = loginAction == ACTION_CREATE)
512+
clearAuthState()
489513
}
490514

491515
private fun loginIsLoading() {
@@ -515,6 +539,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
515539
}
516540
}
517541
}
542+
clearAuthState()
518543
}
519544

520545
/**
@@ -570,6 +595,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
570595
)
571596

572597
try {
598+
saveAuthState()
573599
customTabsIntent.launchUrl(
574600
this,
575601
authorizationEndpointUri
@@ -894,4 +920,26 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
894920
override fun optionLockSelected(type: LockType) {
895921
manageOptionLockSelected(type)
896922
}
923+
924+
private fun saveAuthState() {
925+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
926+
prefs.edit().apply {
927+
putString(KEY_CODE_VERIFIER, authenticationViewModel.codeVerifier)
928+
putString(KEY_CODE_CHALLENGE, authenticationViewModel.codeChallenge)
929+
putString(KEY_OIDC_STATE, authenticationViewModel.oidcState)
930+
apply()
931+
}
932+
}
933+
934+
private fun restoreAuthState() {
935+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
936+
prefs.getString(KEY_CODE_VERIFIER, null)?.let { authenticationViewModel.codeVerifier = it }
937+
prefs.getString(KEY_CODE_CHALLENGE, null)?.let { authenticationViewModel.codeChallenge = it }
938+
prefs.getString(KEY_OIDC_STATE, null)?.let { authenticationViewModel.oidcState = it }
939+
}
940+
941+
private fun clearAuthState() {
942+
val prefs = getSharedPreferences("auth_state", android.content.Context.MODE_PRIVATE)
943+
prefs.edit().clear().apply()
944+
}
897945
}

0 commit comments

Comments
 (0)