[PM-20306] Migrate Auth Token Interceptor (#5065)

This commit is contained in:
Patrick Honkonen
2025-04-16 12:41:43 -04:00
committed by GitHub
parent 3311086dfc
commit 0be26c1eda
11 changed files with 176 additions and 42 deletions

View File

@@ -0,0 +1,8 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.network.interceptor.AuthTokenProvider
/**
* A manager class for handling authentication tokens.
*/
interface AuthTokenManager : AuthTokenProvider

View File

@@ -0,0 +1,17 @@
package com.x8bit.bitwarden.data.auth.manager
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
/**
* Default implementation of [AuthTokenManager].
*/
class AuthTokenManagerImpl(
private val authDiskSource: AuthDiskSource,
) : AuthTokenManager {
override fun getActiveAccessTokenOrNull(): String? = authDiskSource
.userState
?.activeUserId
?.let { authDiskSource.getAccountTokens(it) }
?.accessToken
}

View File

@@ -14,6 +14,8 @@ import com.x8bit.bitwarden.data.auth.manager.AuthRequestManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManager
import com.x8bit.bitwarden.data.auth.manager.AuthRequestNotificationManagerImpl
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManagerImpl
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManager
import com.x8bit.bitwarden.data.auth.manager.KeyConnectorManagerImpl
import com.x8bit.bitwarden.data.auth.manager.TrustedDeviceManager
@@ -131,4 +133,10 @@ object AuthManagerModule {
@Singleton
fun providesAddTotpItemFromAuthenticatorManager(): AddTotpItemFromAuthenticatorManager =
AddTotpItemFromAuthenticatorManagerImpl()
@Provides
@Singleton
fun providesAuthTokenManager(
authDiskSource: AuthDiskSource,
): AuthTokenManager = AuthTokenManagerImpl(authDiskSource = authDiskSource)
}

View File

@@ -1,5 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.di
import com.bitwarden.network.interceptor.AuthTokenInterceptor
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.bitwarden.network.service.ConfigService
import com.bitwarden.network.service.ConfigServiceImpl
@@ -8,8 +9,8 @@ import com.bitwarden.network.service.EventServiceImpl
import com.bitwarden.network.service.PushService
import com.bitwarden.network.service.PushServiceImpl
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.AuthTokenManager
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl
@@ -63,8 +64,10 @@ object PlatformNetworkModule {
@Provides
@Singleton
fun providesAuthTokenInterceptor(
authDiskSource: AuthDiskSource,
): AuthTokenInterceptor = AuthTokenInterceptor(authDiskSource = authDiskSource)
authTokenManager: AuthTokenManager,
): AuthTokenInterceptor = AuthTokenInterceptor(
authTokenProvider = authTokenManager,
)
@Provides
@Singleton

View File

@@ -1,12 +1,12 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.bitwarden.network.core.NetworkResultCallAdapterFactory
import com.bitwarden.network.interceptor.AuthTokenInterceptor
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.platform.util.isDevBuild
import kotlinx.serialization.json.Json

View File

@@ -0,0 +1,104 @@
package com.x8bit.bitwarden.data.auth.manager
import com.bitwarden.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertNull
import java.time.ZonedDateTime
class AuthTokenManagerTest {
private val fakeAuthDiskSource = FakeAuthDiskSource()
private val authTokenManager = AuthTokenManagerImpl(fakeAuthDiskSource)
@Test
fun `UserState is null`() {
fakeAuthDiskSource.userState = null
assertNull(authTokenManager.getActiveAccessTokenOrNull())
}
@Test
fun `Account tokens are null`() {
fakeAuthDiskSource.userState = SINGLE_USER_STATE
.copy(
accounts = mapOf(
USER_ID to ACCOUNT.copy(tokens = null),
),
)
assertNull(authTokenManager.getActiveAccessTokenOrNull())
}
@Test
fun `Access token is null`() {
fakeAuthDiskSource.userState = SINGLE_USER_STATE
.copy(
accounts = mapOf(
USER_ID to ACCOUNT.copy(
tokens = AccountTokensJson(
accessToken = null,
refreshToken = null,
),
),
),
)
assertNull(authTokenManager.getActiveAccessTokenOrNull())
}
@Test
fun `getActiveAccessTokenOrNull should return active user access token`() {
fakeAuthDiskSource.userState = SINGLE_USER_STATE
fakeAuthDiskSource.storeAccountTokens(
userId = USER_ID,
accountTokens = AccountTokensJson(
accessToken = ACCESS_TOKEN,
refreshToken = REFRESH_TOKEN,
),
)
assertEquals(
ACCESS_TOKEN,
authTokenManager.getActiveAccessTokenOrNull(),
)
}
}
private const val EMAIL: String = "test@bitwarden.com"
private const val USER_ID: String = "2a135b23-e1fb-42c9-bec3-573857bc8181"
private const val ACCESS_TOKEN: String = "accessToken"
private const val REFRESH_TOKEN: String = "refreshToken"
private val ACCOUNT: AccountJson = AccountJson(
profile = AccountJson.Profile(
userId = USER_ID,
email = EMAIL,
isEmailVerified = true,
name = "Bitwarden Tester",
hasPremium = false,
stamp = null,
organizationId = null,
avatarColorHex = null,
forcePasswordResetReason = null,
kdfType = KdfTypeJson.ARGON2_ID,
kdfIterations = 600000,
kdfMemory = 16,
kdfParallelism = 4,
userDecryptionOptions = null,
isTwoFactorEnabled = false,
creationDate = ZonedDateTime.parse("2024-09-13T01:00:00.00Z"),
),
tokens = AccountTokensJson(
accessToken = ACCESS_TOKEN,
refreshToken = REFRESH_TOKEN,
),
settings = AccountJson.Settings(
environmentUrlData = null,
),
)
private val SINGLE_USER_STATE: UserStateJson = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(
USER_ID to ACCOUNT,
),
)

View File

@@ -1,10 +1,10 @@
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
import com.bitwarden.network.interceptor.AuthTokenInterceptor
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.bitwarden.network.model.NetworkResult
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.BaseUrlInterceptors
import com.bitwarden.network.interceptor.HeadersInterceptor
import com.x8bit.bitwarden.data.platform.datasource.network.ssl.SslManager
import com.x8bit.bitwarden.data.util.mockBuilder
import io.mockk.every

View File

@@ -2,8 +2,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.android.library)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}
android {
@@ -46,6 +48,8 @@ dependencies {
implementation(project(":core"))
implementation(libs.androidx.core.ktx)
implementation(libs.google.hilt.android)
ksp(libs.google.hilt.compiler)
implementation(libs.kotlinx.serialization)
implementation(libs.square.okhttp)
implementation(libs.square.okhttp.logging)

View File

@@ -1,8 +1,7 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
package com.bitwarden.network.interceptor
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
@@ -13,30 +12,19 @@ import javax.inject.Singleton
*/
@Singleton
class AuthTokenInterceptor(
private val authDiskSource: AuthDiskSource,
private val authTokenProvider: AuthTokenProvider,
) : Interceptor {
/**
* The auth token to be added to API requests.
*
* Note: This is done on demand to ensure that no race conditions can exist when retrieving the
* token.
*/
private val authToken: String?
get() = authDiskSource
.userState
?.activeUserId
?.let { userId -> authDiskSource.getAccountTokens(userId = userId)?.accessToken }
private val missingTokenMessage = "Auth token is missing!"
override fun intercept(chain: Interceptor.Chain): Response {
val token = authToken ?: throw IOException(IllegalStateException(missingTokenMessage))
val token = authTokenProvider.getActiveAccessTokenOrNull()
?: throw IOException(IllegalStateException(missingTokenMessage))
val request = chain
.request()
.newBuilder()
.addHeader(
name = HEADER_KEY_AUTHORIZATION,
value = "$HEADER_VALUE_BEARER_PREFIX$token",
value = "${HEADER_VALUE_BEARER_PREFIX}$token",
)
.build()
return chain

View File

@@ -0,0 +1,12 @@
package com.bitwarden.network.interceptor
/**
* A provider for all the functionality needed to properly refresh the users access token.
*/
interface AuthTokenProvider {
/**
* The currently active user's access token.
*/
fun getActiveAccessTokenOrNull(): String?
}

View File

@@ -1,9 +1,6 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
package com.bitwarden.network.interceptor
import com.bitwarden.network.interceptor.FakeInterceptorChain
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import io.mockk.every
import io.mockk.mockk
import junit.framework.TestCase.assertEquals
import okhttp3.Request
@@ -14,9 +11,11 @@ import javax.inject.Singleton
@Singleton
class AuthTokenInterceptorTest {
private val authDiskSource = FakeAuthDiskSource()
private val mockAuthTokenProvider = mockk<AuthTokenProvider> {
every { getActiveAccessTokenOrNull() } returns null
}
private val interceptor: AuthTokenInterceptor = AuthTokenInterceptor(
authDiskSource = authDiskSource,
authTokenProvider = mockAuthTokenProvider,
)
private val request: Request = Request
.Builder()
@@ -25,8 +24,8 @@ class AuthTokenInterceptorTest {
@Test
fun `intercept should add the auth token when set`() {
authDiskSource.userState = USER_STATE
authDiskSource.storeAccountTokens(userId = USER_ID, ACCOUNT_TOKENS)
every { mockAuthTokenProvider.getActiveAccessTokenOrNull() } returns ACCESS_TOKEN
val response = interceptor.intercept(
chain = FakeInterceptorChain(request = request),
)
@@ -50,13 +49,4 @@ class AuthTokenInterceptorTest {
}
}
private const val USER_ID: String = "user_id"
private const val ACCESS_TOKEN: String = "access_token"
private val USER_STATE: UserStateJson = UserStateJson(
activeUserId = USER_ID,
accounts = mapOf(USER_ID to mockk()),
)
private val ACCOUNT_TOKENS: AccountTokensJson = AccountTokensJson(
accessToken = ACCESS_TOKEN,
refreshToken = null,
)