Skip to content

Commit

Permalink
Merge pull request #3129 from CruGlobal/googleAuthRefresh
Browse files Browse the repository at this point in the history
GT-2153 try refreshing the google id_token if it is rejected
  • Loading branch information
frett authored Oct 6, 2023
2 parents 97d9019 + 6cf3d96 commit 6a3887d
Show file tree
Hide file tree
Showing 2 changed files with 117 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.core.content.edit
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.common.api.ApiException
import dagger.hilt.android.qualifiers.ApplicationContext
import javax.inject.Inject
import javax.inject.Singleton
Expand All @@ -20,6 +21,9 @@ import org.cru.godtools.account.AccountType
import org.cru.godtools.account.provider.AccountProvider
import org.cru.godtools.api.AuthApi
import org.cru.godtools.api.model.AuthToken
import timber.log.Timber

private const val TAG = "GoogleAccountProvider"

private const val PREFS_GOOGLE_ACCOUNT_PROVIDER = "org.godtools.account.google"
private const val PREF_USER_ID_PREFIX = "user_id_"
Expand Down Expand Up @@ -52,18 +56,32 @@ internal class GoogleAccountProvider @Inject constructor(
state.activity.startActivity(googleSignInClient.signInIntent)
}

private suspend fun refreshSignIn() = try {
googleSignInClient.silentSignIn().await()
} catch (e: ApiException) {
Timber.tag(TAG).d(e, "Error refreshing google account authentication")
null
}

override suspend fun logout() {
googleSignInClient.signOut().await()
}
// endregion Login/Logout

override suspend fun authenticateWithMobileContentApi(): AuthToken? {
val account = GoogleSignIn.getLastSignedInAccount(context)
val request = account?.idToken?.let { AuthToken.Request(googleIdToken = it) } ?: return null
val token = authApi.authenticate(request).takeIf { it.isSuccessful }
?.body()?.takeUnless { it.hasErrors }
?.dataSingle
if (token != null) prefs.edit { putString(PREF_USER_ID(account), token.userId) }
var account = GoogleSignIn.getLastSignedInAccount(context) ?: return null
var resp = account.authenticateWithMobileContentApi()

if (account.idToken == null || resp?.isSuccessful != true) {
account = refreshSignIn() ?: return null
resp = account.authenticateWithMobileContentApi() ?: return null
}

val token = resp.takeIf { it.isSuccessful }?.body()?.takeUnless { it.hasErrors }?.dataSingle ?: return null
prefs.edit { putString(PREF_USER_ID(account), token.userId) }
return token
}

private suspend fun GoogleSignInAccount.authenticateWithMobileContentApi() =
idToken?.let { authApi.authenticate(AuthToken.Request(googleIdToken = it)) }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
import app.cash.turbine.test
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.auth.api.signin.GoogleSignInClient
import com.google.android.gms.tasks.Tasks
import io.mockk.Called
import io.mockk.coEvery
import io.mockk.coVerifyAll
import io.mockk.coVerifySequence
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkObject
Expand All @@ -23,15 +29,27 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import okhttp3.ResponseBody.Companion.toResponseBody
import org.ccci.gto.android.common.jsonapi.model.JsonApiObject
import org.ccci.gto.android.common.play.auth.signin.GoogleSignInKtx
import org.cru.godtools.api.AuthApi
import org.cru.godtools.api.model.AuthToken
import org.junit.runner.RunWith
import retrofit2.Response

private const val ID_TOKEN_INVALID = "invalid"
private const val ID_TOKEN_VALID = "valid"

@RunWith(AndroidJUnit4::class)
@OptIn(ExperimentalCoroutinesApi::class)
class GoogleAccountProviderTest {
private val lastSignedInAccount = MutableStateFlow<GoogleSignInAccount?>(null)
private val userId = UUID.randomUUID().toString()

private val authApi: AuthApi = mockk()
private val context: Context get() = ApplicationProvider.getApplicationContext()
private val googleSignInClient: GoogleSignInClient = mockk()

private lateinit var provider: GoogleAccountProvider

@BeforeTest
Expand All @@ -40,7 +58,11 @@ class GoogleAccountProviderTest {
every { GoogleSignInKtx.getLastSignedInAccountFlow(any()) } returns lastSignedInAccount
mockkStatic(GoogleSignIn::class)
every { GoogleSignIn.getLastSignedInAccount(any()) } answers { lastSignedInAccount.value }
provider = GoogleAccountProvider(mockk(), context, mockk())
provider = GoogleAccountProvider(
authApi = authApi,
context = context,
googleSignInClient = googleSignInClient,
)
}

@AfterTest
Expand All @@ -53,7 +75,6 @@ class GoogleAccountProviderTest {
@Test
fun `userIdFlow()`() = runTest {
val account = GoogleSignInAccount.createDefault()
val userId = UUID.randomUUID().toString()
provider.prefs.edit { putString(GoogleAccountProvider.PREF_USER_ID(account), userId) }

provider.userIdFlow().test {
Expand Down Expand Up @@ -81,10 +102,9 @@ class GoogleAccountProviderTest {
lastSignedInAccount.value = account
runCurrent()

val userId1 = UUID.randomUUID().toString()
provider.prefs.edit { putString(GoogleAccountProvider.PREF_USER_ID(account), userId1) }
provider.prefs.edit { putString(GoogleAccountProvider.PREF_USER_ID(account), userId) }
runCurrent()
assertEquals(userId1, expectMostRecentItem())
assertEquals(userId, expectMostRecentItem())

val userId2 = UUID.randomUUID().toString()
provider.prefs.edit { putString(GoogleAccountProvider.PREF_USER_ID(account), userId2) }
Expand All @@ -93,4 +113,72 @@ class GoogleAccountProviderTest {
}
}
// endregion userIdFlow()

// region authenticateWithMobileContentApi()
private val authToken = AuthToken(userId, "token")
private val validAccount: GoogleSignInAccount = mockk {
every { id } returns UUID.randomUUID().toString()
every { idToken } returns ID_TOKEN_VALID
}

@BeforeTest
fun `Setup authenticateWithMobileContentApi()`() {
every { googleSignInClient.silentSignIn() } returns Tasks.forResult(validAccount)

coEvery { authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_VALID)) }
.returns(Response.success(JsonApiObject.single(authToken)))
coEvery { authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_INVALID)) }
.returns(Response.error(401, "".toResponseBody()))
}

@Test
fun `authenticateWithMobileContentApi()`() = runTest {
lastSignedInAccount.value = validAccount

assertEquals(authToken, provider.authenticateWithMobileContentApi())
coVerifySequence {
authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_VALID))

googleSignInClient wasNot Called
}
assertEquals(
userId,
provider.prefs.getString(GoogleAccountProvider.PREF_USER_ID(lastSignedInAccount.value!!), "")
)
}

@Test
fun `authenticateWithMobileContentApi() - Not authenticated`() = runTest {
lastSignedInAccount.value = null

assertNull(provider.authenticateWithMobileContentApi())
coVerifyAll {
authApi wasNot Called
googleSignInClient wasNot Called
}
}

@Test
fun `authenticateWithMobileContentApi() - No id_token`() = runTest {
lastSignedInAccount.value = mockk { every { idToken } returns null }

assertEquals(authToken, provider.authenticateWithMobileContentApi())
coVerifySequence {
googleSignInClient.silentSignIn()
authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_VALID))
}
}

@Test
fun `authenticateWithMobileContentApi() - invalid id_token`() = runTest {
lastSignedInAccount.value = mockk { every { idToken } returns ID_TOKEN_INVALID }

assertEquals(authToken, provider.authenticateWithMobileContentApi())
coVerifySequence {
authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_INVALID))
googleSignInClient.silentSignIn()
authApi.authenticate(AuthToken.Request(googleIdToken = ID_TOKEN_VALID))
}
}
// endregion authenticateWithMobileContentApi()
}

0 comments on commit 6a3887d

Please sign in to comment.