Harden Google sign-in when backend auth fails
Sign out of Firebase and clear API and local profile if the GraphQL exchange or token save fails after Firebase Google sign-in, so the user is not left in a half-authenticated state. Rename the exchange helper to exchangeFirebaseIdTokenWithBackend for clarity. Login: ignore duplicate Google taps while loading and disable the Google button during the flow. Credential Manager: treat GetCredentialInterruptedException as cancellation and rethrow CancellationException so coroutines cancel correctly. Tests: assert signOut is invoked when the backend exchange fails. Made-with: Cursor
This commit is contained in:
@@ -84,9 +84,25 @@ class FirebaseAuthService @Inject constructor(
|
||||
private suspend fun signInWithGoogle(token: String): User {
|
||||
localEmailAuthStore.clearProfile().getOrThrow()
|
||||
val firebaseSessionUser = firebaseSessionManager.signInWithGoogle(token)
|
||||
val apiToken = exchangeFirebaseTokenWithBackend(firebaseSessionUser.firebaseAccessToken)
|
||||
apiTokenStore.saveToken(apiToken).getOrThrow()
|
||||
return firebaseSessionUser.user
|
||||
try {
|
||||
val apiToken = exchangeFirebaseIdTokenWithBackend(firebaseSessionUser.firebaseAccessToken)
|
||||
apiTokenStore.saveToken(apiToken).getOrThrow()
|
||||
return firebaseSessionUser.user
|
||||
} catch (e: Throwable) {
|
||||
rollbackSessionAfterFailedGoogleBackendAuth()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/** Firebase may already be signed in when the backend exchange fails; clear everything so the user stays logged out. */
|
||||
private suspend fun rollbackSessionAfterFailedGoogleBackendAuth() {
|
||||
try {
|
||||
firebaseSessionManager.signOut()
|
||||
apiTokenStore.clearToken().getOrThrow()
|
||||
localEmailAuthStore.clearProfile().getOrThrow()
|
||||
} catch (_: Throwable) {
|
||||
// Best-effort; caller still receives the original sign-in failure.
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun commitEmailAuthSession(
|
||||
@@ -114,9 +130,9 @@ class FirebaseAuthService @Inject constructor(
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun exchangeFirebaseTokenWithBackend(firebaseAccessToken: String): String {
|
||||
private suspend fun exchangeFirebaseIdTokenWithBackend(firebaseIdToken: String): String {
|
||||
val response = apolloClient
|
||||
.mutation(AuthGoogleNewMutation(accessToken = firebaseAccessToken))
|
||||
.mutation(AuthGoogleNewMutation(accessToken = firebaseIdToken))
|
||||
.execute()
|
||||
response.exception?.let { throw it }
|
||||
response.errors?.firstOrNull()?.let { gqlError ->
|
||||
|
||||
@@ -51,6 +51,7 @@ class FirebaseAuthServiceTest {
|
||||
assertTrue(result.isFailure)
|
||||
assertEquals("backend failed", result.exceptionOrNull()?.message)
|
||||
assertEquals(null, tokenStore.storedToken)
|
||||
assertEquals(1, sessionManager.signOutCallCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -131,6 +132,9 @@ private class FakeFirebaseSessionManager(
|
||||
private val signInError: Throwable? = null,
|
||||
private val isSignedIn: Boolean = true
|
||||
) : FirebaseSessionManager {
|
||||
var signOutCallCount = 0
|
||||
private set
|
||||
|
||||
override suspend fun getUser(): User? = signInResult?.user
|
||||
|
||||
override suspend fun isSignedIn(): Boolean = isSignedIn
|
||||
@@ -140,7 +144,9 @@ private class FakeFirebaseSessionManager(
|
||||
return signInResult ?: error("No signInResult configured")
|
||||
}
|
||||
|
||||
override suspend fun signOut() = Unit
|
||||
override suspend fun signOut() {
|
||||
signOutCallCount++
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeApiTokenStore(
|
||||
|
||||
Reference in New Issue
Block a user