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

Commit 27468a60 authored by Abhishek Aggarwal's avatar Abhishek Aggarwal
Browse files

fix(auth): clear stale AAS token on rejected Play auth

parent e0d3eba7
Loading
Loading
Loading
Loading
+11 −1
Original line number Diff line number Diff line
@@ -69,7 +69,7 @@ class PlayStoreStoredAuthPolicy {
            }

            is PlayStoreAuthValidationResult.InvalidAuth -> {
                clearPersistedAuth()
                clearRejectedAuthAndBootstrapState()
                Resolution.RefreshRequired
            }

@@ -114,6 +114,16 @@ class PlayStoreStoredAuthPolicy {

    suspend fun clearPersistedAuth() {
        playStoreAuthStore.saveAuthData(null)
        clearCachedValidation()
    }

    suspend fun clearRejectedAuthAndBootstrapState() {
        playStoreAuthStore.saveAuthData(null)
        playStoreAuthStore.saveAasToken("")
        clearCachedValidation()
    }

    private suspend fun clearCachedValidation() {
        cachedValidationMutex.withLock {
            cachedValidation = null
        }
+1 −1
Original line number Diff line number Diff line
@@ -108,7 +108,7 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {

            val failure = authResult.toAuthError(AuthStore.PLAY_STORE)
            if (shouldEvictPersistedAuth(failure)) {
                playStoreStoredAuthPolicy.clearPersistedAuth()
                playStoreStoredAuthPolicy.clearRejectedAuthAndBootstrapState()
            }
            Timber.e("Play Store token refresh failed: %s", failure.describe())
            AuthResult.Failure(failure)
+1 −14
Original line number Diff line number Diff line
@@ -107,10 +107,8 @@ class AuthenticatorRepository @Inject constructor(

            when (val resolution = playStoreStoredAuthPolicy.resolveStoredAuth()) {
                PlayStoreStoredAuthPolicy.Resolution.Missing,
                PlayStoreStoredAuthPolicy.Resolution.RefreshRequired -> {
                    clearInvalidPlayStoreBootstrapState()
                PlayStoreStoredAuthPolicy.Resolution.RefreshRequired ->
                    rebuildPlayStoreAuth(authenticator)
                }

                is PlayStoreStoredAuthPolicy.Resolution.UseStoredAuth ->
                    ResultSupreme.Success<AuthData?>(resolution.authData)
@@ -137,17 +135,6 @@ class AuthenticatorRepository @Inject constructor(
        return persistedAuthResult.toAuthDataResult()
    }

    private suspend fun clearInvalidPlayStoreBootstrapState() {
        if (sessionRepository.awaitLoginIntent().toPlayStoreLoginModeOrNull() == null) {
            return
        }

        // If validated Play auth is definitively invalid, the persisted AAS token can be stale.
        // Clearing it forces rebuild to prefer a fresh oauth->AAS conversion (or fail fast)
        // instead of silently recreating auth from the same rejected bootstrap token.
        playStoreAuthStore.saveAasToken("")
    }

    private suspend fun persistAuthData(
        storeType: AuthStore,
        authResult: StoreAuthResult,
+64 −0
Original line number Diff line number Diff line
@@ -847,6 +847,70 @@ class PlayStoreAuthenticatorTest {
        coVerify(exactly = 1) { anonymousLoginManager.login() }
    }

    @Test
    fun login_clearsStaleAasTokenBeforeRebuildingInvalidGoogleAuth() = runTest {
        val context = applicationContext()
        val savedAuth = AuthData(email = "user@gmail.com", authToken = "stale-token")
        val freshAuth = AuthData(email = "user@gmail.com", authToken = "fresh-token")
        val sessionRepository = FakeSessionRepository(
            initialLoginIntent = PersistedLoginIntent.PLAY_GOOGLE,
            currentAuthData = savedAuth,
            persistedAuthData = savedAuth,
            email = "user@gmail.com",
            oauthToken = "oauth-token",
            aasToken = "stale-aas-token",
            playStoreAuthSource = PlayStoreAuthSource.GOOGLE,
        )
        val googleLoginManager = mockk<GoogleLoginManager>()
        val aasTokenConverter = mock<OauthToAasTokenConverter>()
        val userProfileFetcher = mockk<UserProfileFetcher>()

        whenever(aasTokenConverter.convert(mockitoAny(), mockitoAny()))
            .thenReturn(ResultSupreme.Success("fresh-aas-token"))
        coEvery {
            googleLoginManager.login(
                email = "user@gmail.com",
                aasToken = "fresh-aas-token",
                oauthToken = "",
            )
        } returns freshAuth
        coEvery { userProfileFetcher.fetch(any()) } returns null

        val authenticator = playStoreAuthenticator(
            context = context,
            sessionRepository = sessionRepository,
            playStoreAuthStore = sessionRepository,
            sourceSelectionRepository = sourceSelectionRepository(),
            authDataCache = AuthDataCache(sessionRepository, json),
            googleLoginManager = googleLoginManager,
            microgLoginManager = mockk(),
            anonymousLoginManager = mockk(),
            aasTokenConverter = aasTokenConverter,
            userProfileFetcher = userProfileFetcher,
            successfulLoginNotifier = NoOpSuccessfulLoginNotifier,
            networkRetryPolicy = NetworkRetryPolicy(),
            playStoreAuthValidator = FakePlayStoreAuthValidator {
                PlayStoreAuthValidationResult.InvalidAuth("stale auth")
            },
        )

        val result = authenticator.login()

        assertThat(result.requirePlayStoreAuthData().authToken).isEqualTo("fresh-token")
        assertThat(result.authDataToPersist?.authToken).isEqualTo("fresh-token")
        assertThat(result.aasTokenToPersist).isEqualTo("fresh-aas-token")
        assertThat(sessionRepository.awaitAasToken()).isEmpty()
        mockitoVerify(aasTokenConverter).convert(mockitoAny(), mockitoAny())
        coVerify(exactly = 0) { googleLoginManager.login() }
        coVerify(exactly = 1) {
            googleLoginManager.login(
                email = "user@gmail.com",
                aasToken = "fresh-aas-token",
                oauthToken = "",
            )
        }
    }

    @Test
    fun login_keepsSavedAuthWhenValidationFailsTransiently() = runTest {
        val context = applicationContext()
+21 −7
Original line number Diff line number Diff line
@@ -96,9 +96,14 @@ class PlayStoreStoredAuthPolicyTest {
    }

    @Test
    fun resolveStoredAuth_clearsPersistedAuthWhenValidationMarksItInvalid() = runTest {
    fun resolveStoredAuth_clearsPersistedAuthAndAasTokenWhenValidationMarksItInvalid() = runTest {
        val authData = buildAuthData()
        val authStore = InMemoryPlayStoreAuthStore(authData)
        val authStore = InMemoryPlayStoreAuthStore(
            initialAuthData = authData,
            initialEmail = "user@example.com",
            initialOauthToken = "oauth-token",
            initialAasToken = "stale-aas-token",
        )
        val validator = CountingFakePlayStoreAuthValidator {
            PlayStoreAuthValidationResult.InvalidAuth("stale auth")
        }
@@ -108,13 +113,19 @@ class PlayStoreStoredAuthPolicyTest {

        assertThat(resolution).isEqualTo(PlayStoreStoredAuthPolicy.Resolution.RefreshRequired)
        assertThat(authStore.awaitAuthData()).isNull()
        assertThat(authStore.awaitAasToken()).isEmpty()
        assertThat(authStore.awaitEmail()).isEqualTo("user@example.com")
        assertThat(authStore.awaitOauthToken()).isEqualTo("oauth-token")
        assertThat(validator.validationCount).isEqualTo(1)
    }

    @Test
    fun resolveStoredAuth_returnsValidationFailureAndKeepsPersistedAuthOnTransientFailure() = runTest {
        val authData = buildAuthData()
        val authStore = InMemoryPlayStoreAuthStore(authData)
        val authStore = InMemoryPlayStoreAuthStore(
            initialAuthData = authData,
            initialAasToken = "cached-aas-token",
        )
        val validator = CountingFakePlayStoreAuthValidator {
            PlayStoreAuthValidationResult.TransientFailure(
                message = "timeout",
@@ -136,6 +147,7 @@ class PlayStoreStoredAuthPolicyTest {
            )
        )
        assertThat(authStore.awaitAuthData()).isEqualTo(authData)
        assertThat(authStore.awaitAasToken()).isEqualTo("cached-aas-token")
    }

    private fun buildAuthData(): AuthData {
@@ -165,11 +177,14 @@ class PlayStoreStoredAuthPolicyTest {

    private class InMemoryPlayStoreAuthStore(
        initialAuthData: AuthData? = null,
        initialEmail: String = initialAuthData?.email.orEmpty(),
        initialOauthToken: String = "",
        initialAasToken: String = "",
    ) : PlayStoreAuthStore {
        private val authDataState = MutableStateFlow(initialAuthData)
        private val emailState = MutableStateFlow(initialAuthData?.email.orEmpty())
        private val oauthTokenState = MutableStateFlow("")
        private val aasTokenState = MutableStateFlow("")
        private val emailState = MutableStateFlow(initialEmail)
        private val oauthTokenState = MutableStateFlow(initialOauthToken)
        private val aasTokenState = MutableStateFlow(initialAasToken)
        private val playStoreAuthSourceState = MutableStateFlow<PlayStoreAuthSource?>(null)

        override val authData: StateFlow<AuthData?> = authDataState
@@ -191,7 +206,6 @@ class PlayStoreStoredAuthPolicyTest {

        override suspend fun saveAuthData(authData: AuthData?) {
            authDataState.value = authData
            emailState.value = authData?.email.orEmpty()
        }

        override suspend fun destroyCredentials() {
Loading