Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit ab904e6d authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix(microg): time out stalled token refresh

parent 6f4125ac
Loading
Loading
Loading
Loading
Loading
+15 −3
Original line number Diff line number Diff line
@@ -32,10 +32,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import foundation.e.apps.data.login.api.LoginManager
import foundation.e.apps.data.login.api.MicrogAccountFetchResult
import foundation.e.apps.data.login.api.MicrogAccountFetcher
import foundation.e.apps.data.login.exceptions.GPlayLoginException
import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.auth.PlayStoreLoginMode
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton

@@ -100,7 +103,7 @@ class MicrogLoginManager @Inject constructor(
                false,
                null,
                null
            ).result
            ).getResult(MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)

            val intent = bundle.parcelableCompat(AccountManager.KEY_INTENT, Intent::class.java)
            if (intent != null) {
@@ -154,11 +157,19 @@ class MicrogLoginManager @Inject constructor(
                playStoreAuthStore.saveAasToken("")
                result.oauthToken
            }
            is MicrogAccountFetchResult.RequiresUserAction -> error(MICROG_TOKEN_REFRESH_FAILURE)
            is MicrogAccountFetchResult.Error -> error(MICROG_TOKEN_REFRESH_FAILURE)
            is MicrogAccountFetchResult.RequiresUserAction -> throwMicrogRefreshFailure()
            is MicrogAccountFetchResult.Error -> throwMicrogRefreshFailure()
        }
    }

    private fun throwMicrogRefreshFailure(): Nothing {
        throw GPlayLoginException(
            isTimeout = false,
            message = MICROG_TOKEN_REFRESH_FAILURE,
            loginMode = PlayStoreLoginMode.MICROG,
        )
    }

    private fun resolveAccount(accounts: Array<Account>, accountName: String): Account {
        return if (accountName.isNotBlank()) {
            accounts.firstOrNull { it.name == accountName } ?: accounts.first()
@@ -189,5 +200,6 @@ class MicrogLoginManager @Inject constructor(
        const val MICROG_TOKEN_PREFIX = "ya29."
        const val MICROG_TOKEN_MISSING = "MicroG token is missing"
        const val MICROG_TOKEN_REFRESH_FAILURE = "MicroG refresh failed"
        const val MICROG_TOKEN_FETCH_TIMEOUT_MILLIS = 10_000L
    }
}
+83 −4
Original line number Diff line number Diff line
@@ -21,13 +21,16 @@ package foundation.e.apps.data.login
import android.accounts.Account
import android.accounts.AccountManager
import android.accounts.AccountManagerFuture
import android.accounts.OperationCanceledException
import android.content.Intent
import android.os.Bundle
import foundation.e.apps.data.login.api.MicrogAccountFetchResult
import foundation.e.apps.data.login.exceptions.GPlayLoginException
import foundation.e.apps.data.login.microg.MicrogCertUtil
import foundation.e.apps.data.login.microg.MicrogLoginManager
import foundation.e.apps.data.login.playstore.OauthAuthDataBuilder
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.data.login.api.MicrogAccountFetchResult
import foundation.e.apps.domain.auth.PlayStoreLoginMode
import kotlinx.coroutines.runBlocking
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
@@ -195,6 +198,30 @@ class MicrogLoginManagerTest {
        assertTrue(result is MicrogAccountFetchResult.Error)
    }

    @Test
    fun `returns error when auth token request times out`() = runBlocking {
        val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)
        val accountManager = mock<AccountManager>()
        val future = TimeoutAccountManagerFuture()
        whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)))
            .thenReturn(arrayOf(account))
        whenever(
            accountManager.getAuthToken(
                eq(account),
                eq(MicrogCertUtil.PLAY_AUTH_SCOPE),
                any<Bundle>(),
                eq(false),
                isNull(),
                isNull()
            )
        ).thenReturn(future)

        val result = buildMicrogLoginManager(accountManager).fetchAccount()

        assertTrue(result is MicrogAccountFetchResult.Error)
        assertEquals(MicrogLoginManager.MICROG_TOKEN_FETCH_TIMEOUT_MILLIS, future.timeoutMillis)
    }

    @Test
    fun `login uses existing oauth token when refresh not needed`() = runBlocking {
        val accountManager = mock<AccountManager>()
@@ -282,13 +309,15 @@ class MicrogLoginManagerTest {
        whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old")
        whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name)

        assertThrows(IllegalStateException::class.java) {
        val exception = assertThrows(GPlayLoginException::class.java) {
            runBlocking {
                buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore)
                    .login()
            }
        }

        assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message)
        assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode)
        org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never())
            .invalidateAuthToken(any(), any())
        org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never())
@@ -317,13 +346,15 @@ class MicrogLoginManagerTest {
        whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old")
        whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name)

        assertThrows(IllegalStateException::class.java) {
        val exception = assertThrows(GPlayLoginException::class.java) {
            runBlocking {
                buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore)
                    .login()
            }
        }

        assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message)
        assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode)
        org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never())
            .invalidateAuthToken(any(), any())
        org.mockito.kotlin.verify(playStoreAuthStore, org.mockito.kotlin.never())
@@ -332,6 +363,39 @@ class MicrogLoginManagerTest {
            .saveAasToken(any())
    }

    @Test
    fun `login routes token fetch timeout to microg login required`() = runBlocking {
        val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)
        val accountManager = mock<AccountManager>()
        whenever(accountManager.getAccountsByType(eq(MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)))
            .thenReturn(arrayOf(account))
        whenever(
            accountManager.getAuthToken(
                eq(account),
                eq(MicrogCertUtil.PLAY_AUTH_SCOPE),
                any<Bundle>(),
                eq(false),
                isNull(),
                isNull()
            )
        ).thenReturn(TimeoutAccountManagerFuture())
        val playStoreAuthStore = mock<PlayStoreAuthStore>()
        whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old")
        whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name)

        val exception = assertThrows(GPlayLoginException::class.java) {
            runBlocking {
                buildMicrogLoginManager(accountManager, playStoreAuthStore = playStoreAuthStore)
                    .login()
            }
        }

        assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message)
        assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode)
        org.mockito.kotlin.verify(accountManager, org.mockito.kotlin.never())
            .invalidateAuthToken(any(), any())
    }

    @Test
    fun `login uses existing oauth token when microg account exists but token is not a microg token`() = runBlocking {
        val account = Account("user@gmail.com", MicrogCertUtil.GOOGLE_ACCOUNT_TYPE)
@@ -398,7 +462,7 @@ class MicrogLoginManagerTest {
        val oauthAuthDataBuilder = mock<OauthAuthDataBuilder>()
        val playStoreAuthStore = mock<PlayStoreAuthStore>()

        val exception = assertThrows(IllegalStateException::class.java) {
        val exception = assertThrows(GPlayLoginException::class.java) {
            runBlocking {
                whenever(playStoreAuthStore.awaitOauthToken()).thenReturn("ya29.old")
                whenever(playStoreAuthStore.awaitEmail()).thenReturn(account.name)
@@ -407,6 +471,7 @@ class MicrogLoginManagerTest {
        }

        assertEquals(MicrogLoginManager.MICROG_TOKEN_REFRESH_FAILURE, exception.message)
        assertEquals(PlayStoreLoginMode.MICROG, exception.loginMode)
    }

    private fun buildMicrogLoginManager(
@@ -436,4 +501,18 @@ class MicrogLoginManagerTest {
        override fun getResult(): Bundle = throw error
        override fun getResult(timeout: Long, unit: TimeUnit): Bundle = throw error
    }

    private class TimeoutAccountManagerFuture : AccountManagerFuture<Bundle> {
        var timeoutMillis: Long = 0
            private set

        override fun cancel(mayInterruptIfRunning: Boolean): Boolean = false
        override fun isCancelled(): Boolean = false
        override fun isDone(): Boolean = false
        override fun getResult(): Bundle = error("Unbounded token fetch must not be used")
        override fun getResult(timeout: Long, unit: TimeUnit): Bundle {
            timeoutMillis = unit.toMillis(timeout)
            throw OperationCanceledException("Timed out")
        }
    }
}