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

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

fix(auth): guard stale refresh credential persistence

parent f5cbe2b1
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -35,7 +35,8 @@ data class StoreAuthResult(
    val storeType: AuthStore,
    val result: ResultSupreme<AuthSession>,
    val authData: AuthData? = null,
    val authDataToPersist: AuthData? = null
    val authDataToPersist: AuthData? = null,
    val aasTokenToPersist: String? = null,
) {
    val session: AuthSession?
        get() = result.data
+98 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2026 e Foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

package foundation.e.apps.data.login.playstore

import com.aurora.gplayapi.data.models.AuthData
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.domain.auth.PersistedLoginIntent
import foundation.e.apps.domain.model.PlayStoreAuthSource
import foundation.e.apps.domain.preferences.SessionRepository
import foundation.e.apps.domain.source.SourceSelection
import foundation.e.apps.domain.source.SourceSelectionRepository
import javax.inject.Inject
import javax.inject.Singleton
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock

@Singleton
class PlayStoreAuthPersistenceGuard @Inject constructor(
    private val sessionRepository: SessionRepository,
    private val sourceSelectionRepository: SourceSelectionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
) {
    private val persistenceMutex = Mutex()

    suspend fun captureContext(): Context {
        return currentContext()
    }

    suspend fun persistPlayCredentialsIfCurrent(
        context: Context,
        authData: AuthData?,
        aasToken: String?,
    ): Boolean {
        if (authData == null && aasToken == null) {
            return true
        }

        return persistenceMutex.withLock {
            if (currentContext() != context) {
                return@withLock false
            }

            aasToken?.let { playStoreAuthStore.saveAasToken(it) }
            authData?.let { playStoreAuthStore.saveAuthData(it) }
            true
        }
    }

    private suspend fun currentContext(): Context {
        return Context(
            loginIntent = sessionRepository.awaitLoginIntent(),
            sourceSelection = sourceSelectionRepository.currentSourceSelection(),
            authDataFingerprint = playStoreAuthStore.awaitAuthData().fingerprint(),
            email = playStoreAuthStore.awaitEmail(),
            oauthToken = playStoreAuthStore.awaitOauthToken(),
            aasToken = playStoreAuthStore.awaitAasToken(),
            authSource = playStoreAuthStore.awaitPlayStoreAuthSource(),
        )
    }

    private fun AuthData?.fingerprint(): String? {
        val authData = this ?: return null
        return listOf(
            authData.email,
            authData.authToken,
            authData.gsfId,
            authData.deviceCheckInConsistencyToken,
            authData.deviceConfigToken,
            authData.dfeCookie,
            authData.isAnonymous.toString(),
        ).joinToString("|")
    }

    data class Context internal constructor(
        val loginIntent: PersistedLoginIntent,
        val sourceSelection: SourceSelection,
        val authDataFingerprint: String?,
        val email: String,
        val oauthToken: String,
        val aasToken: String,
        val authSource: PlayStoreAuthSource?,
    )
}
+3 −4
Original line number Diff line number Diff line
@@ -360,10 +360,9 @@ class PlayStoreAuthenticator @Inject constructor(
        return if (!bootstrapResult.isSuccess() || bootstrapResult.data == null) {
            buildErrorAuthResult(authDataResult)
        } else {
            bootstrapResult.data?.derivedAasToken?.let { derivedAasToken ->
                playStoreAuthStore.saveAasToken(derivedAasToken)
            }
            buildAuthResult(loginMode, bootstrapResult.data?.authData, null)
            buildAuthResult(loginMode, bootstrapResult.data?.authData, null).copy(
                aasTokenToPersist = bootstrapResult.data?.derivedAasToken,
            )
        }
    }

+21 −1
Original line number Diff line number Diff line
@@ -40,6 +40,7 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {
    }

    private val authDataCache: AuthDataCache
    private val playStoreAuthPersistenceGuard: PlayStoreAuthPersistenceGuard
    private val playStoreAuthenticator: PlayStoreAuthenticator
    private val playStoreStoredAuthPolicy: PlayStoreStoredAuthPolicy
    private val monotonicClockMs: () -> Long
@@ -47,10 +48,12 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {
    @Inject
    constructor(
        authDataCache: AuthDataCache,
        playStoreAuthPersistenceGuard: PlayStoreAuthPersistenceGuard,
        playStoreAuthenticator: PlayStoreAuthenticator,
        playStoreStoredAuthPolicy: PlayStoreStoredAuthPolicy,
    ) : this(
        authDataCache = authDataCache,
        playStoreAuthPersistenceGuard = playStoreAuthPersistenceGuard,
        playStoreAuthenticator = playStoreAuthenticator,
        playStoreStoredAuthPolicy = playStoreStoredAuthPolicy,
        monotonicClockMs = { TimeUnit.NANOSECONDS.toMillis(System.nanoTime()) },
@@ -58,11 +61,13 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {

    constructor(
        authDataCache: AuthDataCache,
        playStoreAuthPersistenceGuard: PlayStoreAuthPersistenceGuard,
        playStoreAuthenticator: PlayStoreAuthenticator,
        playStoreStoredAuthPolicy: PlayStoreStoredAuthPolicy,
        monotonicClockMs: () -> Long,
    ) {
        this.authDataCache = authDataCache
        this.playStoreAuthPersistenceGuard = playStoreAuthPersistenceGuard
        this.playStoreAuthenticator = playStoreAuthenticator
        this.playStoreStoredAuthPolicy = playStoreStoredAuthPolicy
        this.monotonicClockMs = monotonicClockMs
@@ -80,13 +85,21 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {
                return@withLock AuthResult.Success(Unit)
            }

            val persistenceContext = playStoreAuthPersistenceGuard.captureContext()
            Timber.w("Refreshing Play Store token after request-time auth failure")
            val refreshResult = playStoreAuthenticator.refreshLogin()
            val authResult = refreshResult.result
            val refreshedAuthData = refreshResult.authDataToPersist ?: refreshResult.authData

            if (authResult.isSuccess() && refreshedAuthData != null) {
                authDataCache.saveAuthData(refreshedAuthData)
                val didPersist = playStoreAuthPersistenceGuard.persistPlayCredentialsIfCurrent(
                    context = persistenceContext,
                    authData = refreshedAuthData,
                    aasToken = refreshResult.aasTokenToPersist,
                )
                if (!didPersist) {
                    return@withLock AuthResult.Failure(staleAuthError())
                }
                playStoreStoredAuthPolicy.recordPersistedAuth(refreshedAuthData)
                markRefreshSuccessful(refreshedAuthData)
                Timber.i("Play Store token refresh succeeded")
@@ -117,4 +130,11 @@ class PlayStoreTokenRefreshHandler : TokenRefreshHandler {
    private fun shouldEvictPersistedAuth(error: AuthError): Boolean {
        return error is AuthError.InvalidToken
    }

    private fun staleAuthError(): AuthError {
        return AuthError.LoginRequired(
            store = AuthStore.PLAY_STORE,
            message = "Play Store auth changed before refreshed credentials could be persisted",
        )
    }
}
+60 −10
Original line number Diff line number Diff line
@@ -25,6 +25,7 @@ import foundation.e.apps.data.login.core.StoreAuthResult
import foundation.e.apps.data.login.core.StoreAuthenticator
import foundation.e.apps.data.login.core.toAuthDataResult
import foundation.e.apps.data.login.exceptions.GPlayLoginException
import foundation.e.apps.data.login.playstore.PlayStoreAuthPersistenceGuard
import foundation.e.apps.data.login.playstore.PlayStoreStoredAuthPolicy
import foundation.e.apps.data.preference.PlayStoreAuthStore
import foundation.e.apps.data.playstore.utils.GPlayHttpClient
@@ -43,6 +44,7 @@ class AuthenticatorRepository @Inject constructor(
    private val authenticators: List<StoreAuthenticator>,
    private val sessionRepository: SessionRepository,
    private val playStoreAuthStore: PlayStoreAuthStore,
    private val playStoreAuthPersistenceGuard: PlayStoreAuthPersistenceGuard,
    private val playStoreStoredAuthPolicy: PlayStoreStoredAuthPolicy,
) : PlayStoreAuthManager, StoreAuthCoordinator {
    private val validatedAuthMutex = Mutex()
@@ -85,9 +87,14 @@ class AuthenticatorRepository @Inject constructor(
                authenticator.logout()
            }

            val persistenceContext = capturePersistenceContext(authenticator.storeType)
            val authResult = authenticator.login()
            authResults.add(authResult)
            persistAuthData(authenticator.storeType, authResult)
            val persistedAuthResult = persistAuthData(
                storeType = authenticator.storeType,
                authResult = authResult,
                persistenceContext = persistenceContext,
            )
            authResults.add(persistedAuthResult)
        }

        return authResults
@@ -120,9 +127,14 @@ class AuthenticatorRepository @Inject constructor(
    private suspend fun rebuildPlayStoreAuth(
        authenticator: StoreAuthenticator,
    ): ResultSupreme<AuthData?> {
        val persistenceContext = playStoreAuthPersistenceGuard.captureContext()
        val authResult = authenticator.login()
        persistAuthData(AuthStore.PLAY_STORE, authResult)
        return authResult.toAuthDataResult()
        val persistedAuthResult = persistAuthData(
            storeType = AuthStore.PLAY_STORE,
            authResult = authResult,
            persistenceContext = persistenceContext,
        )
        return persistedAuthResult.toAuthDataResult()
    }

    private suspend fun clearInvalidPlayStoreBootstrapState() {
@@ -139,12 +151,50 @@ class AuthenticatorRepository @Inject constructor(
    private suspend fun persistAuthData(
        storeType: AuthStore,
        authResult: StoreAuthResult,
    ) {
        authResult.authDataToPersist?.let { authData ->
            playStoreAuthStore.saveAuthData(authData)
            if (storeType == AuthStore.PLAY_STORE && authResult.result.isSuccess()) {
        persistenceContext: PlayStoreAuthPersistenceGuard.Context?,
    ): StoreAuthResult {
        val authData = authResult.authDataToPersist
        val aasToken = authResult.aasTokenToPersist
        if (authData == null && aasToken == null) {
            return authResult
        }

        if (storeType != AuthStore.PLAY_STORE || persistenceContext == null) {
            authData?.let { playStoreAuthStore.saveAuthData(it) }
            return authResult
        }

        val didPersist = playStoreAuthPersistenceGuard.persistPlayCredentialsIfCurrent(
            context = persistenceContext,
            authData = authData,
            aasToken = aasToken,
        )
        if (!didPersist) {
            return staleAuthResult(storeType)
        }

        if (authData != null && authResult.result.isSuccess()) {
            playStoreStoredAuthPolicy.recordPersistedAuth(authData)
        }
        return authResult
    }

    private suspend fun capturePersistenceContext(
        storeType: AuthStore,
    ): PlayStoreAuthPersistenceGuard.Context? {
        return if (storeType == AuthStore.PLAY_STORE) {
            playStoreAuthPersistenceGuard.captureContext()
        } else {
            null
        }
    }

    private fun staleAuthResult(storeType: AuthStore): StoreAuthResult {
        return StoreAuthResult(
            storeType = storeType,
            result = ResultSupreme.Error(
                "Play Store auth changed before refreshed credentials could be persisted"
            ),
        )
    }
}
Loading