Skip to content

Commit 168ffb7

Browse files
committed
Login: Fix OAuth crash and UX after process death
When Android kills the app process while the user is in the browser for OAuth login, the OAuth redirect creates a fresh LoginActivity with no ViewModel state. This caused a MalformedURLException because the token endpoint URL was built from an empty serverBaseUrl. Unlike activity death (where onSaveInstanceState Bundle survives), process death requires SharedPreferences to persist state across the browser redirect. The existing commit feba750 saved PKCE state to SharedPreferences but missed serverBaseUrl. Fix by: - Saving serverBaseUrl and oidcSupported to SharedPreferences alongside the existing PKCE state - On OAuth redirect after process death, re-running OIDC discovery (getServerInfo) to repopulate the ViewModel before processing the authorization code - Always launching FileDisplayActivity after successful account discovery, so the user lands in the app instead of Chrome coming back to foreground (cherry picked from commit a7b0863)
1 parent 1f4c4c7 commit 168ffb7

1 file changed

Lines changed: 52 additions & 12 deletions

File tree

  • opencloudApp/src/main/java/eu/opencloud/android/presentation/authentication

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

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ private const val KEY_OIDC_SUPPORTED = "KEY_OIDC_SUPPORTED"
9999
private const val KEY_CODE_VERIFIER = "KEY_CODE_VERIFIER"
100100
private const val KEY_CODE_CHALLENGE = "KEY_CODE_CHALLENGE"
101101
private const val KEY_OIDC_STATE = "KEY_OIDC_STATE"
102+
private const val KEY_AUTH_SERVER_BASE_URL = "KEY_AUTH_SERVER_BASE_URL"
103+
private const val KEY_AUTH_OIDC_SUPPORTED = "KEY_AUTH_OIDC_SUPPORTED"
104+
private const val KEY_AUTH_LOGIN_ACTION = "KEY_AUTH_LOGIN_ACTION"
105+
private const val KEY_AUTH_USER_ACCOUNT = "KEY_AUTH_USER_ACCOUNT"
102106

103107
class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrustedCertListener, SecurityEnforced {
104108

@@ -237,14 +241,19 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
237241
if (savedInstanceState == null) {
238242
restoreAuthState()
239243
}
240-
handleGetAuthorizationCodeResponse(intent)
244+
if (authenticationViewModel.serverInfo.value?.peekContent()?.getStoredData() == null
245+
&& ::serverBaseUrl.isInitialized && serverBaseUrl.isNotEmpty()) {
246+
// Process death: serverInfo is gone. Re-fetch it before processing the OAuth response.
247+
// Store the intent as pending — getServerInfoIsSuccess will process it via checkServerType bypass.
248+
pendingAuthorizationIntent = intent
249+
authenticationViewModel.getServerInfo(serverBaseUrl)
250+
} else {
251+
handleGetAuthorizationCodeResponse(intent)
252+
}
241253
}
242254

243-
// Process any pending intent that arrived before binding was ready
244-
pendingAuthorizationIntent?.let {
245-
handleGetAuthorizationCodeResponse(it)
246-
pendingAuthorizationIntent = null
247-
}
255+
// Note: pendingAuthorizationIntent is processed in checkServerType() after
256+
// getServerInfo() completes (process death recovery flow).
248257

249258

250259
}
@@ -262,7 +271,10 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
262271

263272
private fun launchFileDisplayActivity() {
264273
val newIntent = Intent(this, FileDisplayActivity::class.java)
265-
newIntent.data = intent.data
274+
if (authenticationViewModel.launchedFromDeepLink) {
275+
newIntent.data = intent.data
276+
}
277+
newIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
266278
startActivity(newIntent)
267279
finish()
268280
}
@@ -296,11 +308,7 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
296308
authenticationViewModel.accountDiscovery.observe(this) {
297309
if (it.peekContent() is UIResult.Success) {
298310
notifyDocumentsProviderRoots(applicationContext)
299-
if (authenticationViewModel.launchedFromDeepLink) {
300-
launchFileDisplayActivity()
301-
} else {
302-
finish()
303-
}
311+
launchFileDisplayActivity()
304312
} else {
305313
binding.authStatusText.run {
306314
text = context.getString(R.string.login_account_preparing)
@@ -425,6 +433,18 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
425433
}
426434

427435
private fun checkServerType(serverInfo: ServerInfo) {
436+
// If we have a pending OAuth response (process death recovery), process it now
437+
// that serverInfo has been re-fetched, instead of starting a new auth flow.
438+
pendingAuthorizationIntent?.let { pendingIntent ->
439+
pendingAuthorizationIntent = null
440+
authTokenType = OAUTH_TOKEN_TYPE
441+
if (serverInfo is ServerInfo.OIDCServer) {
442+
oidcSupported = true
443+
}
444+
handleGetAuthorizationCodeResponse(pendingIntent)
445+
return
446+
}
447+
428448
when (serverInfo) {
429449
is ServerInfo.BasicServer -> {
430450
authTokenType = BASIC_TOKEN_TYPE
@@ -1016,6 +1036,15 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
10161036
putString(KEY_CODE_VERIFIER, authenticationViewModel.codeVerifier)
10171037
putString(KEY_CODE_CHALLENGE, authenticationViewModel.codeChallenge)
10181038
putString(KEY_OIDC_STATE, authenticationViewModel.oidcState)
1039+
if (::serverBaseUrl.isInitialized) {
1040+
putString(KEY_AUTH_SERVER_BASE_URL, serverBaseUrl)
1041+
}
1042+
putBoolean(KEY_AUTH_OIDC_SUPPORTED, oidcSupported)
1043+
// The browser is also opened for re-login (ACTION_UPDATE_TOKEN / ACTION_UPDATE_EXPIRED_TOKEN),
1044+
// not just for new account creation. Persist these so the redirect back knows
1045+
// whether to update an existing account or create a new one.
1046+
putInt(KEY_AUTH_LOGIN_ACTION, loginAction.toInt())
1047+
userAccount?.name?.let { putString(KEY_AUTH_USER_ACCOUNT, it) }
10191048
apply()
10201049
}
10211050
}
@@ -1025,6 +1054,17 @@ class LoginActivity : AppCompatActivity(), SslUntrustedCertDialog.OnSslUntrusted
10251054
prefs.getString(KEY_CODE_VERIFIER, null)?.let { authenticationViewModel.codeVerifier = it }
10261055
prefs.getString(KEY_CODE_CHALLENGE, null)?.let { authenticationViewModel.codeChallenge = it }
10271056
prefs.getString(KEY_OIDC_STATE, null)?.let { authenticationViewModel.oidcState = it }
1057+
prefs.getString(KEY_AUTH_SERVER_BASE_URL, null)?.let { serverBaseUrl = it }
1058+
oidcSupported = prefs.getBoolean(KEY_AUTH_OIDC_SUPPORTED, false)
1059+
val savedLoginAction = prefs.getInt(KEY_AUTH_LOGIN_ACTION, -1)
1060+
if (savedLoginAction != -1) {
1061+
loginAction = savedLoginAction.toByte()
1062+
}
1063+
if (userAccount == null) {
1064+
prefs.getString(KEY_AUTH_USER_ACCOUNT, null)?.let { accountName ->
1065+
userAccount = Account(accountName, getString(R.string.account_type))
1066+
}
1067+
}
10281068
}
10291069

10301070
private fun clearAuthState() {

0 commit comments

Comments
 (0)