diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManager.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManager.kt new file mode 100644 index 0000000000..9b0a1b36c8 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManager.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerImpl.kt new file mode 100644 index 0000000000..6cd83275e4 --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerImpl.kt @@ -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 +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/di/AuthManagerModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/di/AuthManagerModule.kt index fc842461f7..87bdf25f7e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/di/AuthManagerModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/manager/di/AuthManagerModule.kt @@ -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) } diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt index 8c8ef296e9..2e82f00fa8 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/di/PlatformNetworkModule.kt @@ -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 diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt index 77dacc44ed..f17feb355f 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsImpl.kt @@ -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 diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerTest.kt new file mode 100644 index 0000000000..26d1b99694 --- /dev/null +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/manager/AuthTokenManagerTest.kt @@ -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, + ), +) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt index a8375f73bb..6c3ad88ae4 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/retrofit/RetrofitsTest.kt @@ -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 diff --git a/network/build.gradle.kts b/network/build.gradle.kts index 862455d09d..05612c70f4 100644 --- a/network/build.gradle.kts +++ b/network/build.gradle.kts @@ -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) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptor.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptor.kt similarity index 50% rename from app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptor.kt rename to network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptor.kt index feb929b76c..44e0872e7e 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptor.kt +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptor.kt @@ -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 diff --git a/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenProvider.kt b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenProvider.kt new file mode 100644 index 0000000000..6847522c0c --- /dev/null +++ b/network/src/main/kotlin/com/bitwarden/network/interceptor/AuthTokenProvider.kt @@ -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? +} diff --git a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptorTest.kt b/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptorTest.kt similarity index 57% rename from app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptorTest.kt rename to network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptorTest.kt index 7e108b549f..cd4c39ed01 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/platform/datasource/network/interceptor/AuthTokenInterceptorTest.kt +++ b/network/src/test/kotlin/com/bitwarden/network/interceptor/AuthTokenInterceptorTest.kt @@ -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 { + 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, -)