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 Fetching : AuthState()
data object Verifying : AuthState()
data class PendingAccountManager(val email: String, val token: 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
*/
suspend fun isSavedAuthDataValid(): Boolean {
// TODO: Switch to the method from gplayapi
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
}
}
fun isSavedAuthDataValid(): Boolean {
return AuthHelper.isValid(authData!!)
}
/**

View File

@@ -2,7 +2,9 @@ package com.aurora.store.data.work
import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AccountManagerCallback
import android.content.Context
import android.os.Bundle
import android.os.Handler
import android.os.Looper
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 dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.take
import kotlinx.coroutines.runBlocking
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
/**
* Worker to refresh [AuthData] in background
@@ -40,8 +41,6 @@ open class AuthWorker @AssistedInject constructor(
private val TAG = AuthWorker::class.java.simpleName
private val authToken: MutableSharedFlow<String?> = MutableSharedFlow(extraBufferCapacity = 1)
override suspend fun doWork(): Result {
if (!AccountProvider.isLoggedIn(appContext)) {
Log.i(TAG, "User has logged out!")
@@ -58,40 +57,23 @@ open class AuthWorker @AssistedInject constructor(
val accountType = AccountProvider.getAccountType(appContext)
val authData = when (accountType) {
AccountType.GOOGLE -> {
val email = AccountProvider.getLoginEmail(appContext)!!
val token = AccountProvider.getLoginToken(appContext)!!.first
val tokenType = AccountProvider.getLoginToken(appContext)!!.second
val email = AccountProvider.getLoginEmail(appContext)
val tokenPair = AccountProvider.getLoginToken(appContext)
if (tokenType == AuthHelper.Token.AAS) {
Log.i(TAG, "Refreshing AuthData for personal account")
authProvider.buildGoogleAuthData(email, token, tokenType).getOrThrow()
} else {
/*
* We are working with AuthToken here. The only scenario when we will have
* AuthToken and Google login is when the user used microG to login into
* Aurora Store. In this case, we use system's AccountManager to request credentials.
*/
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
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,
{
authToken.tryEmit(it.result.getString(AccountManager.KEY_AUTHTOKEN))
},
Handler(Looper.getMainLooper())
)
runBlocking {
authProvider.buildGoogleAuthData(
email,
authToken.take(1).first()!!,
tokenType
).getOrThrow()
if (email == null || tokenPair == null) {
throw Exception()
}
when (tokenPair.second) {
AuthHelper.Token.AAS -> {
Log.i(TAG, "Refreshing AuthData for personal account")
authProvider.buildGoogleAuthData(email, tokenPair.first, AuthHelper.Token.AAS).getOrThrow()
}
AuthHelper.Token.AUTH -> {
Log.i(TAG, "Refreshing AuthData for personal account using AccountManager")
val newToken = fetchAuthToken(email, tokenPair.first)
authProvider.buildGoogleAuthData(email, newToken, AuthHelper.Token.AAS).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? {
return if (authData.authToken.isNotEmpty() && authData.deviceConfigToken.isNotEmpty()) {
authProvider.saveAuthData(authData)

View File

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

View File

@@ -128,35 +128,41 @@ class AuthViewModel @Inject constructor(
}
}
private fun buildSavedAuthData() {
viewModelScope.launch(Dispatchers.IO) {
try {
if (authProvider.isSavedAuthDataValid()) {
_authState.value = AuthState.Valid
} else {
// Generate and validate new auth
when (AccountProvider.getAccountType(context)) {
AccountType.GOOGLE -> {
buildGoogleAuthData(
AccountProvider.getLoginEmail(context)!!,
AccountProvider.getLoginToken(context)!!.first,
AccountProvider.getLoginToken(context)!!.second
)
private fun buildSavedAuthData() = viewModelScope.launch(Dispatchers.IO) {
try {
if (authProvider.isSavedAuthDataValid()) {
_authState.value = AuthState.Valid
} else {
// Generate and validate new auth
when (AccountProvider.getAccountType(context)) {
AccountType.ANONYMOUS -> buildAnonymousAuthData()
AccountType.GOOGLE -> {
val email = AccountProvider.getLoginEmail(context)
val tokenPair = AccountProvider.getLoginToken(context)
if (email == null || tokenPair == null) {
throw Exception()
}
AccountType.ANONYMOUS -> {
buildAnonymousAuthData()
when (tokenPair.second) {
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)
}
}