mirror of
https://github.com/bitwarden/android.git
synced 2026-03-12 05:04:17 -05:00
[PM-20306] Migrate Auth Token Interceptor (#5065)
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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?
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user