improve microG account login & token refresh logic

This commit is contained in:
Rahul Patel
2025-03-02 13:54:00 +00:00
parent 0ead570a70
commit 585284fb10
5 changed files with 121 additions and 77 deletions

View File

@@ -41,5 +41,6 @@ sealed class AuthState {
data object Valid : AuthState() data object Valid : AuthState()
data object Fetching : AuthState() data object Fetching : AuthState()
data object Verifying : AuthState() data object Verifying : AuthState()
data class PendingAccountManager(val email: String, val token: String) : AuthState()
data class Failed(val status: String) : AuthState() data class Failed(val status: String) : AuthState()
} }

View File

@@ -72,20 +72,8 @@ class AuthProvider @Inject constructor(
/** /**
* Checks whether saved AuthData is valid or not * Checks whether saved AuthData is valid or not
*/ */
suspend fun isSavedAuthDataValid(): Boolean { fun isSavedAuthDataValid(): Boolean {
// TODO: Switch to the method from gplayapi return AuthHelper.isValid(authData!!)
return withContext(Dispatchers.IO) {
try {
val testPackageName = "com.android.chrome"
val app = AppDetailsHelper(authData!!)
.using(httpClient)
.getAppByPackageName(testPackageName)
app.packageName == testPackageName && app.displayName.isNotBlank()
&& app.versionCode != 0
} catch (exception: Exception) {
false
}
}
} }
/** /**

View File

@@ -2,7 +2,9 @@ package com.aurora.store.data.work
import android.accounts.Account import android.accounts.Account
import android.accounts.AccountManager import android.accounts.AccountManager
import android.accounts.AccountManagerCallback
import android.content.Context import android.content.Context
import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Base64 import android.util.Base64
@@ -22,10 +24,9 @@ import com.aurora.store.util.CertUtil.GOOGLE_PLAY_CERT
import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME import com.aurora.store.util.CertUtil.GOOGLE_PLAY_PACKAGE_NAME
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow import kotlin.coroutines.resume
import kotlinx.coroutines.flow.first import kotlin.coroutines.resumeWithException
import kotlinx.coroutines.flow.take import kotlin.coroutines.suspendCoroutine
import kotlinx.coroutines.runBlocking
/** /**
* Worker to refresh [AuthData] in background * Worker to refresh [AuthData] in background
@@ -40,8 +41,6 @@ open class AuthWorker @AssistedInject constructor(
private val TAG = AuthWorker::class.java.simpleName private val TAG = AuthWorker::class.java.simpleName
private val authToken: MutableSharedFlow<String?> = MutableSharedFlow(extraBufferCapacity = 1)
override suspend fun doWork(): Result { override suspend fun doWork(): Result {
if (!AccountProvider.isLoggedIn(appContext)) { if (!AccountProvider.isLoggedIn(appContext)) {
Log.i(TAG, "User has logged out!") Log.i(TAG, "User has logged out!")
@@ -58,40 +57,23 @@ open class AuthWorker @AssistedInject constructor(
val accountType = AccountProvider.getAccountType(appContext) val accountType = AccountProvider.getAccountType(appContext)
val authData = when (accountType) { val authData = when (accountType) {
AccountType.GOOGLE -> { AccountType.GOOGLE -> {
val email = AccountProvider.getLoginEmail(appContext)!! val email = AccountProvider.getLoginEmail(appContext)
val token = AccountProvider.getLoginToken(appContext)!!.first val tokenPair = AccountProvider.getLoginToken(appContext)
val tokenType = AccountProvider.getLoginToken(appContext)!!.second
if (tokenType == AuthHelper.Token.AAS) { if (email == null || tokenPair == null) {
Log.i(TAG, "Refreshing AuthData for personal account") throw Exception()
authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow() }
} else {
/* when (tokenPair.second) {
* We are working with AuthToken here. The only scenario when we will have AuthHelper.Token.AAS -> {
* AuthToken and Google login is when the user used microG to login into Log.i(TAG, "Refreshing AuthData for personal account")
* Aurora Store. In this case, we use system's AccountManager to request credentials. authProvider.buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS).getOrThrow()
*/ }
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
AccountManager.get(appContext) AuthHelper.Token.AUTH -> {
.getAuthToken( Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
Account(email, GOOGLE_ACCOUNT_TYPE), val newToken = fetchAuthToken(email, tokenPair.first)
GOOGLE_PLAY_AUTH_TOKEN_TYPE, authProvider.buildGoogleAuthData(email, newToken, AuthHelper.Token.AAS).getOrThrow()
bundleOf(
"overridePackage" to GOOGLE_PLAY_PACKAGE_NAME,
"overrideCertificate" to Base64.decode(GOOGLE_PLAY_CERT, Base64.DEFAULT)
),
true,
{
authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN))
},
Handler(Looper.getMainLooper())
)
runBlocking {
authProvider.buildGoogleAuthData(
email,
authToken.take(1).first()!!,
tokenType
).getOrThrow()
} }
} }
} }
@@ -111,6 +93,61 @@ open class AuthWorker @AssistedInject constructor(
} }
} }
private suspend fun fetchAuthToken(email: String, oldToken: String? = null): String {
return suspendCoroutine { continuation ->
fetchAuthToken(email, oldToken) { future ->
try {
val bundle = future.result
val token = bundle.getString(AccountManager.KEY_AUTHTOKEN)
if (token != null) {
continuation.resume(token)
} else {
continuation.resumeWithException(IllegalStateException("Auth token is null"))
}
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
}
}
private fun fetchAuthToken(
email: String,
oldToken: String? = null,
callback: AccountManagerCallback<Bundle>
) {
try {
if (oldToken != null) {
// Invalidate the old token before requesting a new one
AccountManager.get(appContext)
.invalidateAuthToken(
GOOGLE_ACCOUNT_TYPE,
oldToken
)
}
AccountManager.get(appContext)
.getAuthToken(
Account(email, GOOGLE_ACCOUNT_TYPE),
GOOGLE_PLAY_AUTH_TOKEN_TYPE,
bundleOf(
"overridePackage" to GOOGLE_PLAY_PACKAGE_NAME,
"overrideCertificate" to Base64.decode(
GOOGLE_PLAY_CERT,
Base64.DEFAULT
)
),
true,
callback,
Handler(Looper.getMainLooper())
)
} catch (e: Exception) {
Log.e(TAG, "Failed to fetch auth token", e)
callback.run(null)
}
}
private fun verifyAndSaveAuth(authData: AuthData, accountType: AccountType): AuthData? { private fun verifyAndSaveAuth(authData: AuthData, accountType: AccountType): AuthData? {
return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) { return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) {
authProvider.saveAuthData(authData) authProvider.saveAuthData(authData)

View File

@@ -173,6 +173,10 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
updateStatus(getString(R.string.verifying_new_session)) updateStatus(getString(R.string.verifying_new_session))
} }
is AuthState.PendingAccountManager -> {
requestAuthTokenForGoogle(it.email, it.token)
}
is AuthState.Failed -> { is AuthState.Failed -> {
updateStatus(it.status) updateStatus(it.status)
updateActionLayout(true) updateActionLayout(true)
@@ -271,8 +275,16 @@ class SplashFragment : BaseFragment<FragmentSplashBinding>() {
} }
} }
private fun requestAuthTokenForGoogle(accountName: String) { private fun requestAuthTokenForGoogle(accountName: String, oldToken: String? = null) {
try { try {
if (oldToken != null) {
// Invalidate the old token before requesting a new one
AccountManager.get(requireContext()).invalidateAuthToken(
GOOGLE_ACCOUNT_TYPE,
oldToken
)
}
AccountManager.get(requireContext()) AccountManager.get(requireContext())
.getAuthToken( .getAuthToken(
Account(accountName, GOOGLE_ACCOUNT_TYPE), Account(accountName, GOOGLE_ACCOUNT_TYPE),

View File

@@ -128,35 +128,41 @@ class AuthViewModel @Inject constructor(
} }
} }
private fun buildSavedAuthData() { private fun buildSavedAuthData() = viewModelScope.launch(Dispatchers.IO) {
viewModelScope.launch(Dispatchers.IO) { try {
try { if (authProvider.isSavedAuthDataValid()) {
if (authProvider.isSavedAuthDataValid()) { _authState.value = AuthState.Valid
_authState.value = AuthState.Valid } else {
} else { // Generate and validate new auth
// Generate and validate new auth when (AccountProvider.getAccountType(context)) {
when (AccountProvider.getAccountType(context)) { AccountType.ANONYMOUS -> buildAnonymousAuthData()
AccountType.GOOGLE -> { AccountType.GOOGLE -> {
buildGoogleAuthData( val email = AccountProvider.getLoginEmail(context)
AccountProvider.getLoginEmail(context)!!, val tokenPair = AccountProvider.getLoginToken(context)
AccountProvider.getLoginToken(context)!!.first,
AccountProvider.getLoginToken(context)!!.second if (email == null || tokenPair == null) {
) throw Exception()
} }
AccountType.ANONYMOUS -> { when (tokenPair.second) {
buildAnonymousAuthData() AuthHelper.Token.AAS -> {
buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS)
}
AuthHelper.Token.AUTH -> {
_authState.value = AuthState.PendingAccountManager(email, tokenPair.first)
}
} }
} }
} }
} catch (exception: Exception) {
val error = when (exception) {
is UnknownHostException -> context.getString(R.string.title_no_network)
is ConnectException -> context.getString(R.string.server_unreachable)
else -> context.getString(R.string.bad_request)
}
_authState.value = AuthState.Failed(error)
} }
} catch (exception: Exception) {
val error = when (exception) {
is UnknownHostException -> context.getString(R.string.title_no_network)
is ConnectException -> context.getString(R.string.server_unreachable)
else -> context.getString(R.string.bad_request)
}
_authState.value = AuthState.Failed(error)
} }
} }