Skip to content

Commit 1c3812f

Browse files
authored
dataconnect: auth token internal refactor to track authUid, part 1 (#7484)
1 parent 968342d commit 1c3812f

File tree

8 files changed

+75
-54
lines changed

8 files changed

+75
-54
lines changed

firebase-dataconnect/CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Unreleased
22

3+
- [changed] Internal refactor for managing Auth and App Check tokens
4+
([#7184](https://github.com/firebase/firebase-android-sdk/pull/7184))
5+
36
# 17.1.0
47

58
- [fixed] Addressed minor reference documentation issues (#7399)

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectAppCheck.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.google.firebase.annotations.DeferredApi
2020
import com.google.firebase.appcheck.AppCheckTokenResult
2121
import com.google.firebase.appcheck.interop.AppCheckTokenListener
2222
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
23+
import com.google.firebase.dataconnect.core.DataConnectAppCheck.GetAppCheckTokenResult
2324
import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken
2425
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
2526
import kotlinx.coroutines.CoroutineDispatcher
@@ -32,7 +33,7 @@ internal class DataConnectAppCheck(
3233
blockingDispatcher: CoroutineDispatcher,
3334
logger: Logger,
3435
) :
35-
DataConnectCredentialsTokenManager<InteropAppCheckTokenProvider>(
36+
DataConnectCredentialsTokenManager<InteropAppCheckTokenProvider, GetAppCheckTokenResult>(
3637
deferredProvider = deferredAppCheckTokenProvider,
3738
parentCoroutineScope = parentCoroutineScope,
3839
blockingDispatcher = blockingDispatcher,
@@ -48,7 +49,9 @@ internal class DataConnectAppCheck(
4849
provider.removeAppCheckTokenListener(appCheckTokenListener)
4950

5051
override suspend fun getToken(provider: InteropAppCheckTokenProvider, forceRefresh: Boolean) =
51-
provider.getToken(forceRefresh).await().let { GetTokenResult(it.token) }
52+
provider.getToken(forceRefresh).await().let { GetAppCheckTokenResult(it.token) }
53+
54+
data class GetAppCheckTokenResult(override val token: String?) : GetTokenResult
5255

5356
private class AppCheckTokenListenerImpl(private val logger: Logger) : AppCheckTokenListener {
5457
override fun onAppCheckTokenChanged(tokenResult: AppCheckTokenResult) {

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectAuth.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package com.google.firebase.dataconnect.core
1919
import com.google.firebase.annotations.DeferredApi
2020
import com.google.firebase.auth.internal.IdTokenListener
2121
import com.google.firebase.auth.internal.InternalAuthProvider
22+
import com.google.firebase.dataconnect.core.DataConnectAuth.GetAuthTokenResult
2223
import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken
2324
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
2425
import com.google.firebase.internal.InternalTokenResult
@@ -32,7 +33,7 @@ internal class DataConnectAuth(
3233
blockingDispatcher: CoroutineDispatcher,
3334
logger: Logger,
3435
) :
35-
DataConnectCredentialsTokenManager<InternalAuthProvider>(
36+
DataConnectCredentialsTokenManager<InternalAuthProvider, GetAuthTokenResult>(
3637
deferredProvider = deferredAuthProvider,
3738
parentCoroutineScope = parentCoroutineScope,
3839
blockingDispatcher = blockingDispatcher,
@@ -48,7 +49,9 @@ internal class DataConnectAuth(
4849
provider.removeIdTokenListener(idTokenListener)
4950

5051
override suspend fun getToken(provider: InternalAuthProvider, forceRefresh: Boolean) =
51-
provider.getAccessToken(forceRefresh).await().let { GetTokenResult(it.token) }
52+
provider.getAccessToken(forceRefresh).await().let { GetAuthTokenResult(it.token) }
53+
54+
data class GetAuthTokenResult(override val token: String?) : GetTokenResult
5255

5356
private class IdTokenListenerImpl(private val logger: Logger) : IdTokenListener {
5457
override fun onIdTokenChanged(tokenResult: InternalTokenResult) {

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectCredentialsTokenManager.kt

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.google.firebase.annotations.DeferredApi
2020
import com.google.firebase.appcheck.interop.InteropAppCheckTokenProvider
2121
import com.google.firebase.auth.internal.InternalAuthProvider
2222
import com.google.firebase.dataconnect.DataConnectException
23+
import com.google.firebase.dataconnect.core.DataConnectCredentialsTokenManager.GetTokenResult
2324
import com.google.firebase.dataconnect.core.Globals.toScrubbedAccessToken
2425
import com.google.firebase.dataconnect.core.LoggerGlobals.debug
2526
import com.google.firebase.dataconnect.core.LoggerGlobals.warn
@@ -52,7 +53,7 @@ import kotlinx.coroutines.flow.update
5253
import kotlinx.coroutines.launch
5354

5455
/** Base class that shares logic for managing the Auth token and AppCheck token. */
55-
internal sealed class DataConnectCredentialsTokenManager<T : Any>(
56+
internal sealed class DataConnectCredentialsTokenManager<T : Any, R : GetTokenResult>(
5657
private val deferredProvider: com.google.firebase.inject.Deferred<T>,
5758
parentCoroutineScope: CoroutineScope,
5859
private val blockingDispatcher: CoroutineDispatcher,
@@ -75,13 +76,13 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
7576
}
7677
)
7778

78-
private sealed interface State<out T> {
79+
private sealed interface State<out T, out R : GetTokenResult> {
7980

8081
/**
8182
* State indicating that the object has just been created and [initialize] has not yet been
8283
* called.
8384
*/
84-
object New : State<Nothing>
85+
object New : State<Nothing, Nothing>
8586

8687
/**
8788
* State indicating that [initialize] has been invoked but the token provider is not (yet?)
@@ -93,33 +94,33 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
9394
}
9495

9596
/** State indicating that [close] has been invoked. */
96-
object Closed : State<Nothing>
97+
object Closed : State<Nothing, Nothing>
9798

98-
sealed interface StateWithForceTokenRefresh<out T> : State<T> {
99+
sealed interface StateWithForceTokenRefresh<out T> : State<T, Nothing> {
99100
/** The value to specify for `forceRefresh` on the next invocation of [getToken]. */
100101
val forceTokenRefresh: Boolean
101102
}
102103

103-
sealed interface StateWithProvider<out T> : State<T> {
104+
sealed interface StateWithProvider<out T, out R : GetTokenResult> : State<T, R> {
104105
/** The token provider, [InternalAuthProvider] or [InteropAppCheckTokenProvider] */
105106
val provider: T
106107
}
107108

108109
/** State indicating that there is no outstanding "get token" request. */
109110
data class Idle<T>(override val provider: T, override val forceTokenRefresh: Boolean) :
110-
StateWithProvider<T>, StateWithForceTokenRefresh<T>
111+
StateWithProvider<T, Nothing>, StateWithForceTokenRefresh<T>
111112

112113
/** State indicating that there _is_ an outstanding "get token" request. */
113-
data class Active<out T>(
114+
data class Active<out T, out R : GetTokenResult>(
114115
override val provider: T,
115116

116117
/** The job that is performing the "get token" request. */
117-
val job: Deferred<SequencedReference<Result<GetTokenResult>>>
118-
) : StateWithProvider<T>
118+
val job: Deferred<SequencedReference<Result<R>>>
119+
) : StateWithProvider<T, R>
119120
}
120121

121122
/** The current state of this object. */
122-
private val state = MutableStateFlow<State<T>>(State.New)
123+
private val state = MutableStateFlow<State<T, R>>(State.New)
123124

124125
/**
125126
* Adds the token listener to the given provider.
@@ -139,7 +140,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
139140
* Starts an asynchronous task to get a new access token from the given provider, forcing a token
140141
* refresh if and only if `forceRefresh` is true.
141142
*/
142-
protected abstract suspend fun getToken(provider: T, forceRefresh: Boolean): GetTokenResult
143+
protected abstract suspend fun getToken(provider: T, forceRefresh: Boolean): R
143144

144145
/**
145146
* Initializes this object.
@@ -274,7 +275,7 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
274275
invocationId: String,
275276
provider: T,
276277
forceRefresh: Boolean
277-
): State.Active<T> {
278+
): State.Active<T, R> {
278279
val coroutineName =
279280
CoroutineName(
280281
"$instanceId 535gmcvv5a $invocationId getToken(" +
@@ -296,14 +297,14 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
296297
* @throws DataConnectException if [close] has been called or is called while the operation is in
297298
* progress.
298299
*/
299-
suspend fun getToken(requestId: String): String? {
300+
suspend fun getToken(requestId: String): R? {
300301
val invocationId = "gat" + Random.nextAlphanumericString(length = 8)
301302
logger.debug { "$invocationId getToken(requestId=$requestId)" }
302303
while (true) {
303304
val attemptSequenceNumber = nextSequenceNumber()
304305
val oldState = state.value
305306

306-
val newState: State.Active<T> =
307+
val newState: State.Active<T, R> =
307308
when (oldState) {
308309
is State.New ->
309310
throw IllegalStateException("initialize() must be called before getToken()")
@@ -381,11 +382,12 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
381382
}
382383
}
383384

384-
val accessToken = sequencedResult!!.ref.getOrThrow().token
385+
val tokenResult: R = sequencedResult!!.ref.getOrThrow()
385386
logger.debug {
386-
"$invocationId getToken() returns retrieved token: ${accessToken?.toScrubbedAccessToken()}"
387+
"$invocationId getToken() returns retrieved token: " +
388+
tokenResult.token?.toScrubbedAccessToken()
387389
}
388-
return accessToken
390+
return tokenResult
389391
}
390392
}
391393

@@ -440,16 +442,17 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
440442
* strong reference to the [DataConnectCredentialsTokenManager] instance indefinitely, in the case
441443
* that the callback never occurs.
442444
*/
443-
private class DeferredProviderHandlerImpl<T : Any>(
444-
private val weakCredentialsTokenManagerRef: WeakReference<DataConnectCredentialsTokenManager<T>>
445+
private class DeferredProviderHandlerImpl<T : Any, R : GetTokenResult>(
446+
private val weakCredentialsTokenManagerRef:
447+
WeakReference<DataConnectCredentialsTokenManager<T, R>>
445448
) : DeferredHandler<T> {
446449
override fun handle(provider: Provider<T>) {
447450
weakCredentialsTokenManagerRef.get()?.onProviderAvailable(provider.get())
448451
}
449452
}
450453

451454
private class CredentialsTokenManagerClosedException(
452-
tokenProvider: DataConnectCredentialsTokenManager<*>
455+
tokenProvider: DataConnectCredentialsTokenManager<*, *>
453456
) :
454457
DataConnectException(
455458
"DataConnectCredentialsTokenManager ${tokenProvider.instanceId} was closed (code cqrbq4zfvy)"
@@ -458,7 +461,9 @@ internal sealed class DataConnectCredentialsTokenManager<T : Any>(
458461
private class GetTokenCancelledException(cause: Throwable) :
459462
DataConnectException("getToken() was cancelled, likely by close() (code rqdd4jam9d)", cause)
460463

461-
protected data class GetTokenResult(val token: String?)
464+
interface GetTokenResult {
465+
val token: String?
466+
}
462467

463468
private companion object {
464469

firebase-dataconnect/src/main/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadata.kt

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,8 @@ internal class DataConnectGrpcMetadata(
8585
if (appId.isNotBlank()) {
8686
it.put(gmpAppIdHeader, appId)
8787
}
88-
if (authToken !== null) {
89-
it.put(firebaseAuthTokenHeader, authToken)
90-
}
91-
if (appCheckToken !== null) {
92-
it.put(firebaseAppCheckTokenHeader, appCheckToken)
93-
}
88+
authToken?.token?.let { token -> it.put(firebaseAuthTokenHeader, token) }
89+
appCheckToken?.token?.let { token -> it.put(firebaseAppCheckTokenHeader, token) }
9490
}
9591
}
9692

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectAuthUnitTest.kt

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import io.kotest.assertions.withClue
4949
import io.kotest.matchers.collections.shouldContain
5050
import io.kotest.matchers.collections.shouldContainExactly
5151
import io.kotest.matchers.nulls.shouldBeNull
52+
import io.kotest.matchers.nulls.shouldNotBeNull
5253
import io.kotest.matchers.shouldBe
5354
import io.kotest.matchers.types.shouldBeSameInstanceAs
5455
import io.kotest.property.Arb
@@ -302,7 +303,7 @@ class DataConnectAuthUnitTest {
302303

303304
val result = dataConnectAuth.getToken(requestId)
304305

305-
withClue("result=$result") { result shouldBe accessToken }
306+
withClue("result=$result") { result.shouldNotBeNull().token shouldBe accessToken }
306307
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(requestId)
307308
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(
308309
"returns retrieved token: ${accessToken.toScrubbedAccessToken()}"
@@ -363,7 +364,7 @@ class DataConnectAuthUnitTest {
363364
dataConnectAuth.forceRefresh()
364365
val result = dataConnectAuth.getToken(requestId)
365366

366-
withClue("result=$result") { result shouldBe accessToken }
367+
withClue("result=$result") { result.shouldNotBeNull().token shouldBe accessToken }
367368
verify(exactly = 1) { mockInternalAuthProvider.getAccessToken(true) }
368369
verify(exactly = 0) { mockInternalAuthProvider.getAccessToken(false) }
369370
mockLogger.shouldHaveLoggedExactlyOneMessageContaining(requestId)
@@ -419,7 +420,7 @@ class DataConnectAuthUnitTest {
419420
taskForToken(accessTokenGenerator.next().also { tokens.add(it) })
420421
}
421422

422-
val results = List(5) { dataConnectAuth.getToken(requestId) }
423+
val results = List(5) { dataConnectAuth.getToken(requestId)?.token }
423424

424425
results shouldContainExactly tokens
425426
}
@@ -447,7 +448,7 @@ class DataConnectAuthUnitTest {
447448
}
448449
}
449450

450-
val actualTokens = jobs.map { it.await() }
451+
val actualTokens = jobs.map { it.await()?.token }
451452
actualTokens.forEachIndexed { index, token ->
452453
withClue("actualTokens[$index]") { tokens shouldContain token }
453454
}
@@ -481,7 +482,7 @@ class DataConnectAuthUnitTest {
481482

482483
val result = dataConnectAuth.getToken(requestId)
483484

484-
withClue("result=$result") { result shouldBe tokens.last() }
485+
withClue("result=$result") { result.shouldNotBeNull().token shouldBe tokens.last() }
485486
verify(exactly = 2) { mockInternalAuthProvider.getAccessToken(true) }
486487
verify(exactly = 1) { mockInternalAuthProvider.getAccessToken(false) }
487488
mockLogger.shouldHaveLoggedAtLeastOneMessageContaining("retrying due to needs token refresh")
@@ -496,11 +497,7 @@ class DataConnectAuthUnitTest {
496497
advanceUntilIdle()
497498
val invocationCount = AtomicInteger(0)
498499
val tokens = CopyOnWriteArrayList<String>()
499-
val getTokenJob2 =
500-
async(start = CoroutineStart.LAZY) {
501-
val accessToken = dataConnectAuth.getToken(requestId)
502-
accessToken
503-
}
500+
val getTokenJob2 = async(start = CoroutineStart.LAZY) { dataConnectAuth.getToken(requestId) }
504501
coEvery { mockInternalAuthProvider.getAccessToken(any()) } coAnswers
505502
{
506503
if (invocationCount.getAndIncrement() == 0) {
@@ -509,16 +506,15 @@ class DataConnectAuthUnitTest {
509506
getTokenJob2.start()
510507
advanceUntilIdle()
511508
}
512-
val rv = taskForToken(accessTokenGenerator.next().also { tokens.add(it) })
513-
rv
509+
taskForToken(accessTokenGenerator.next().also { tokens.add(it) })
514510
}
515511

516512
val result1 = dataConnectAuth.getToken(requestId)
517513
withClue("getTokenJob2.isActive") { getTokenJob2.isActive shouldBe true }
518514
val result2 = getTokenJob2.await()
519515

520-
withClue("result1=$result1") { result1 shouldBe tokens[0] }
521-
withClue("result2=$result2") { result2 shouldBe tokens[1] }
516+
withClue("result1=$result1") { result1.shouldNotBeNull().token shouldBe tokens[0] }
517+
withClue("result2=$result2") { result2.shouldNotBeNull().token shouldBe tokens[1] }
522518
verify(exactly = 2) { mockInternalAuthProvider.getAccessToken(false) }
523519
verify(exactly = 0) { mockInternalAuthProvider.getAccessToken(true) }
524520
mockLogger.shouldHaveLoggedExactlyOneMessageContaining("got an old result; retrying")

firebase-dataconnect/src/test/kotlin/com/google/firebase/dataconnect/core/DataConnectGrpcMetadataUnitTest.kt

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4
2020
import com.google.firebase.dataconnect.BuildConfig
2121
import com.google.firebase.dataconnect.FirebaseDataConnect.CallerSdkType
2222
import com.google.firebase.dataconnect.testutil.FirebaseAppUnitTestingRule
23+
import com.google.firebase.dataconnect.testutil.property.arbitrary.appCheckTokenResult
24+
import com.google.firebase.dataconnect.testutil.property.arbitrary.authTokenResult
2325
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnect
2426
import com.google.firebase.dataconnect.testutil.property.arbitrary.dataConnectGrpcMetadata
2527
import io.grpc.Metadata
@@ -175,8 +177,8 @@ class DataConnectGrpcMetadataUnitTest {
175177
@Test
176178
fun `should include x-firebase-auth-token when the auth token is not null`() = runTest {
177179
val dataConnectAuth: DataConnectAuth = mockk()
178-
val accessToken = Arb.dataConnect.accessToken().next()
179-
coEvery { dataConnectAuth.getToken(any()) } returns accessToken
180+
val authTokenResult = Arb.dataConnect.authTokenResult().next()
181+
coEvery { dataConnectAuth.getToken(any()) } returns authTokenResult
180182
val dataConnectGrpcMetadata =
181183
Arb.dataConnect
182184
.dataConnectGrpcMetadata(dataConnectAuth = Arb.constant(dataConnectAuth))
@@ -189,7 +191,7 @@ class DataConnectGrpcMetadataUnitTest {
189191
metadata.asClue {
190192
it.keys() shouldContain "x-firebase-auth-token"
191193
val metadataKey = Metadata.Key.of("x-firebase-auth-token", Metadata.ASCII_STRING_MARSHALLER)
192-
it.get(metadataKey) shouldBe accessToken
194+
it.get(metadataKey) shouldBe authTokenResult.token
193195
}
194196
}
195197

@@ -212,9 +214,9 @@ class DataConnectGrpcMetadataUnitTest {
212214

213215
@Test
214216
fun `should include x-firebase-appcheck when the AppCheck token is not null`() = runTest {
215-
val accessToken = Arb.dataConnect.accessToken().next()
217+
val appCheckTokenResult = Arb.dataConnect.appCheckTokenResult().next()
216218
val dataConnectAppCheck: DataConnectAppCheck = mockk {
217-
coEvery { getToken(any()) } returns accessToken
219+
coEvery { getToken(any()) } returns appCheckTokenResult
218220
}
219221
val dataConnectGrpcMetadata =
220222
Arb.dataConnect
@@ -228,7 +230,7 @@ class DataConnectGrpcMetadataUnitTest {
228230
metadata.asClue {
229231
it.keys() shouldContain "x-firebase-appcheck"
230232
val metadataKey = Metadata.Key.of("x-firebase-appcheck", Metadata.ASCII_STRING_MARSHALLER)
231-
it.get(metadataKey) shouldBe accessToken
233+
it.get(metadataKey) shouldBe appCheckTokenResult.token
232234
}
233235
}
234236

0 commit comments

Comments
 (0)