mirror of
https://github.com/bitwarden/android.git
synced 2025-12-11 04:39:19 -06:00
[PM-20549] Introduce BitwardenServiceClient (#5091)
This commit is contained in:
parent
cd11164544
commit
0f6d15d6a6
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.disk
|
||||
|
||||
import com.bitwarden.network.model.SyncResponseJson
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountTokensJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.PendingAuthRequestJson
|
||||
@ -12,7 +13,7 @@ import java.time.Instant
|
||||
* Primary access point for disk information.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
interface AuthDiskSource {
|
||||
interface AuthDiskSource : AppIdProvider {
|
||||
|
||||
/**
|
||||
* The currently persisted authenticator sync symmetric key. This key is used for
|
||||
@ -20,13 +21,6 @@ interface AuthDiskSource {
|
||||
*/
|
||||
var authenticatorSyncSymmetricKey: ByteArray?
|
||||
|
||||
/**
|
||||
* Retrieves a unique ID for the application that is stored locally. This will generate a new
|
||||
* one if it does not yet exist and it will only be reset for new installs or when clearing
|
||||
* application data.
|
||||
*/
|
||||
val uniqueAppId: String
|
||||
|
||||
/**
|
||||
* The currently persisted saved email address (or `null` if not set).
|
||||
*/
|
||||
|
||||
@ -1,26 +1,17 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.di
|
||||
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AccountsServiceImpl
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.AuthRequestsServiceImpl
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
import com.bitwarden.network.service.DevicesServiceImpl
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedService
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedServiceImpl
|
||||
import com.bitwarden.network.service.IdentityService
|
||||
import com.bitwarden.network.service.IdentityServiceImpl
|
||||
import com.bitwarden.network.service.NewAuthRequestService
|
||||
import com.bitwarden.network.service.NewAuthRequestServiceImpl
|
||||
import com.bitwarden.network.service.OrganizationService
|
||||
import com.bitwarden.network.service.OrganizationServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.create
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@ -33,70 +24,42 @@ object AuthNetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountService(
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): AccountsService = AccountsServiceImpl(
|
||||
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
|
||||
authenticatedKeyConnectorApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = true)
|
||||
.create(),
|
||||
json = json,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): AccountsService = bitwardenServiceClient.accountsService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthRequestsService(
|
||||
retrofits: Retrofits,
|
||||
): AuthRequestsService = AuthRequestsServiceImpl(
|
||||
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): AuthRequestsService = bitwardenServiceClient.authRequestsService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesDevicesService(
|
||||
retrofits: Retrofits,
|
||||
): DevicesService = DevicesServiceImpl(
|
||||
authenticatedDevicesApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedDevicesApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): DevicesService = bitwardenServiceClient.devicesService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityService(
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(
|
||||
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): IdentityService = bitwardenServiceClient.identityService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesHaveIBeenPwnedService(
|
||||
retrofits: Retrofits,
|
||||
): HaveIBeenPwnedService = HaveIBeenPwnedServiceImpl(
|
||||
api = retrofits
|
||||
.createStaticRetrofit(baseUrl = "https://api.pwnedpasswords.com")
|
||||
.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): HaveIBeenPwnedService = bitwardenServiceClient.haveIBeenPwnedService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesNewAuthRequestService(
|
||||
retrofits: Retrofits,
|
||||
): NewAuthRequestService = NewAuthRequestServiceImpl(
|
||||
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedAuthRequestsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): NewAuthRequestService = bitwardenServiceClient.newAuthRequestService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesOrganizationService(
|
||||
retrofits: Retrofits,
|
||||
): OrganizationService = OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): OrganizationService = bitwardenServiceClient.organizationService
|
||||
}
|
||||
|
||||
@ -402,7 +402,11 @@ class AuthRepositoryImpl(
|
||||
.syncOrgKeysFlow
|
||||
.onEach {
|
||||
val userId = activeUserId ?: return@onEach
|
||||
refreshAccessTokenSynchronously(userId)
|
||||
// TODO: [PM-20593] Investigate why tokens are explicitly refreshed.
|
||||
refreshAccessTokenSynchronouslyInternal(
|
||||
userId = userId,
|
||||
logOutOnFailure = false,
|
||||
)
|
||||
vaultRepository.sync(forced = true)
|
||||
}
|
||||
// This requires the ioScope to ensure that refreshAccessTokenSynchronously
|
||||
@ -755,33 +759,11 @@ class AuthRepositoryImpl(
|
||||
orgIdentifier = organizationIdentifier,
|
||||
)
|
||||
|
||||
override fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson> {
|
||||
val refreshToken = authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.refreshToken
|
||||
?: return IllegalStateException("Must be logged in.").asFailure()
|
||||
return identityService
|
||||
.refreshTokenSynchronously(refreshToken)
|
||||
.flatMap { refreshTokenResponse ->
|
||||
// Check to make sure the user is still logged in after making the request
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(userId)
|
||||
?.let { refreshTokenResponse.asSuccess() }
|
||||
?: IllegalStateException("Must be logged in.").asFailure()
|
||||
}
|
||||
.onSuccess { refreshTokenResponse ->
|
||||
// Update the existing UserState with updated token information
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = refreshTokenResponse.accessToken,
|
||||
refreshToken = refreshTokenResponse.refreshToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
override fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson> =
|
||||
refreshAccessTokenSynchronouslyInternal(
|
||||
userId = userId,
|
||||
logOutOnFailure = true,
|
||||
)
|
||||
|
||||
override fun logout(reason: LogoutReason) {
|
||||
activeUserId?.let { userId -> logout(userId = userId, reason = reason) }
|
||||
@ -1433,6 +1415,42 @@ class AuthRepositoryImpl(
|
||||
onFailure = { LeaveOrganizationResult.Error(error = it) },
|
||||
)
|
||||
|
||||
private fun refreshAccessTokenSynchronouslyInternal(
|
||||
userId: String,
|
||||
logOutOnFailure: Boolean,
|
||||
): Result<RefreshTokenResponseJson> {
|
||||
val refreshToken = authDiskSource
|
||||
.getAccountTokens(userId = userId)
|
||||
?.refreshToken
|
||||
?: return IllegalStateException("Must be logged in.").asFailure()
|
||||
return identityService
|
||||
.refreshTokenSynchronously(refreshToken)
|
||||
.flatMap { refreshTokenResponse ->
|
||||
// Check to make sure the user is still logged in after making the request
|
||||
authDiskSource
|
||||
.userState
|
||||
?.accounts
|
||||
?.get(userId)
|
||||
?.let { refreshTokenResponse.asSuccess() }
|
||||
?: IllegalStateException("Must be logged in.").asFailure()
|
||||
}
|
||||
.onFailure {
|
||||
if (logOutOnFailure) {
|
||||
logout(userId = userId, reason = LogoutReason.TokenRefreshFail)
|
||||
}
|
||||
}
|
||||
.onSuccess { refreshTokenResponse ->
|
||||
// Update the existing UserState with updated token information
|
||||
authDiskSource.storeAccountTokens(
|
||||
userId = userId,
|
||||
accountTokens = AccountTokensJson(
|
||||
accessToken = refreshTokenResponse.accessToken,
|
||||
refreshToken = refreshTokenResponse.refreshToken,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("CyclomaticComplexMethod")
|
||||
private suspend fun validatePasswordAgainstPolicy(
|
||||
password: String,
|
||||
|
||||
@ -2,6 +2,7 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.network.model.GetTokenResponseJson
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
package com.x8bit.bitwarden.data.autofill.fido2.datasource.network.di
|
||||
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.network.service.DigitalAssetLinkServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import retrofit2.create
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@ -20,11 +18,7 @@ object Fido2NetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDigitalAssetLinkService(
|
||||
retrofits: Retrofits,
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): DigitalAssetLinkService =
|
||||
DigitalAssetLinkServiceImpl(
|
||||
digitalAssetLinkApi = retrofits
|
||||
.createStaticRetrofit()
|
||||
.create(),
|
||||
)
|
||||
bitwardenServiceClient.digitalAssetLinkService
|
||||
}
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.authenticator
|
||||
|
||||
import com.bitwarden.network.model.RefreshTokenResponseJson
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
|
||||
/**
|
||||
* A provider for all the functionality needed to properly refresh the users access token.
|
||||
*/
|
||||
interface AuthenticatorProvider {
|
||||
interface AuthenticatorProvider : RefreshTokenProvider {
|
||||
|
||||
/**
|
||||
* The currently active user's ID.
|
||||
@ -17,12 +17,4 @@ interface AuthenticatorProvider {
|
||||
* Attempts to logout the user based on the [userId].
|
||||
*/
|
||||
fun logout(userId: String, reason: LogoutReason)
|
||||
|
||||
/**
|
||||
* Attempt to refresh the user's access token based on the [userId].
|
||||
*
|
||||
* This call is both synchronous and performs a network request. Make sure that you are calling
|
||||
* from an appropriate thread.
|
||||
*/
|
||||
fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson>
|
||||
}
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.authenticator
|
||||
|
||||
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* An authenticator used to refresh the access token when a 401 is returned from an API. Upon
|
||||
* successfully getting a new access token, the original request is retried.
|
||||
*/
|
||||
@Singleton
|
||||
class RefreshAuthenticator : Authenticator {
|
||||
|
||||
/**
|
||||
* A provider required to update tokens.
|
||||
*/
|
||||
var authenticatorProvider: AuthenticatorProvider? = null
|
||||
|
||||
override fun authenticate(
|
||||
route: Route?,
|
||||
response: Response,
|
||||
): Request? {
|
||||
val accessToken = requireNotNull(
|
||||
response
|
||||
.request
|
||||
.header(name = HEADER_KEY_AUTHORIZATION)
|
||||
?.substringAfter(delimiter = HEADER_VALUE_BEARER_PREFIX),
|
||||
)
|
||||
return when (val userId = parseJwtTokenDataOrNull(accessToken)?.userId) {
|
||||
null -> {
|
||||
// We unable to get the user ID, let's just let the 401 pass through.
|
||||
null
|
||||
}
|
||||
|
||||
authenticatorProvider?.activeUserId -> {
|
||||
// In order to prevent potential deadlocks or thread starvation we want the call
|
||||
// to refresh the access token to be strictly synchronous with no internal thread
|
||||
// hopping.
|
||||
authenticatorProvider
|
||||
?.refreshAccessTokenSynchronously(userId)
|
||||
?.fold(
|
||||
onFailure = {
|
||||
authenticatorProvider?.logout(
|
||||
userId = userId,
|
||||
reason = LogoutReason.TokenRefreshFail,
|
||||
)
|
||||
null
|
||||
},
|
||||
onSuccess = {
|
||||
response.request
|
||||
.newBuilder()
|
||||
.header(
|
||||
name = HEADER_KEY_AUTHORIZATION,
|
||||
value = "$HEADER_VALUE_BEARER_PREFIX${it.accessToken}",
|
||||
)
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
// We are no longer the active user, let's just cancel.
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +1,24 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.di
|
||||
|
||||
import com.bitwarden.network.interceptor.AuthTokenInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.bitwardenServiceClient
|
||||
import com.bitwarden.network.interceptor.BaseUrlsProvider
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.ConfigServiceImpl
|
||||
import com.bitwarden.network.service.EventService
|
||||
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.retrofit.Retrofits
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.RetrofitsImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_NAME
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
|
||||
import com.x8bit.bitwarden.data.platform.manager.CertificateManager
|
||||
import com.x8bit.bitwarden.data.platform.util.isDevBuild
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.create
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@ -38,70 +32,42 @@ object PlatformNetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigService(
|
||||
retrofits: Retrofits,
|
||||
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): ConfigService = bitwardenServiceClient.configService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesEventService(
|
||||
retrofits: Retrofits,
|
||||
): EventService = EventServiceImpl(
|
||||
eventApi = retrofits.authenticatedEventsRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): EventService = bitwardenServiceClient.eventService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providePushService(
|
||||
retrofits: Retrofits,
|
||||
authDiskSource: AuthDiskSource,
|
||||
): PushService = PushServiceImpl(
|
||||
pushApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
appId = authDiskSource.uniqueAppId,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): PushService = bitwardenServiceClient.pushService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAuthTokenInterceptor(
|
||||
fun provideBitwardenServiceClient(
|
||||
authTokenManager: AuthTokenManager,
|
||||
): AuthTokenInterceptor = AuthTokenInterceptor(
|
||||
authTokenProvider = authTokenManager,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesHeadersInterceptor(): HeadersInterceptor = HeadersInterceptor(
|
||||
userAgent = HEADER_VALUE_USER_AGENT,
|
||||
clientName = HEADER_VALUE_CLIENT_NAME,
|
||||
clientVersion = HEADER_VALUE_CLIENT_VERSION,
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesRefreshAuthenticator(): RefreshAuthenticator = RefreshAuthenticator()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesBaseUrlInterceptors(
|
||||
baseUrlsProvider: BaseUrlsProvider,
|
||||
): BaseUrlInterceptors =
|
||||
BaseUrlInterceptors(baseUrlsProvider = baseUrlsProvider)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofits(
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
authDiskSource: AuthDiskSource,
|
||||
certificateManager: CertificateManager,
|
||||
json: Json,
|
||||
): Retrofits =
|
||||
RetrofitsImpl(
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptor,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
clock: Clock,
|
||||
): BitwardenServiceClient = bitwardenServiceClient(
|
||||
BitwardenServiceClientConfig(
|
||||
clock = clock,
|
||||
appIdProvider = authDiskSource,
|
||||
clientData = BitwardenServiceClientConfig.ClientData(
|
||||
userAgent = HEADER_VALUE_USER_AGENT,
|
||||
clientName = HEADER_VALUE_CLIENT_NAME,
|
||||
clientVersion = HEADER_VALUE_CLIENT_VERSION,
|
||||
),
|
||||
authTokenProvider = authTokenManager,
|
||||
baseUrlsProvider = baseUrlsProvider,
|
||||
certificateProvider = certificateManager,
|
||||
json = json,
|
||||
)
|
||||
enableHttpBodyLogging = isDevBuild,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
|
||||
/**
|
||||
* Interface for managing SSL connections.
|
||||
*/
|
||||
interface SslManager {
|
||||
|
||||
/**
|
||||
* The SSL context to use for SSL connections.
|
||||
*/
|
||||
val sslContext: SSLContext
|
||||
|
||||
/**
|
||||
* The trust managers to use for SSL connections.
|
||||
*/
|
||||
val trustManagers: Array<TrustManager>
|
||||
}
|
||||
@ -6,6 +6,7 @@ import androidx.core.content.getSystemService
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManagerImpl
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.EventService
|
||||
import com.bitwarden.network.service.PushService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
|
||||
@ -17,7 +18,6 @@ import com.x8bit.bitwarden.data.platform.datasource.disk.EventDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
|
||||
import com.x8bit.bitwarden.data.platform.datasource.disk.legacy.LegacyAppCenterMigrator
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManager
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppResumeManagerImpl
|
||||
import com.x8bit.bitwarden.data.platform.manager.AppStateManager
|
||||
@ -244,14 +244,14 @@ object PlatformManagerModule {
|
||||
authRepository: AuthRepository,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
dispatcherManager: DispatcherManager,
|
||||
): NetworkConfigManager =
|
||||
NetworkConfigManagerImpl(
|
||||
authRepository = authRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
bitwardenServiceClient = bitwardenServiceClient,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
|
||||
|
||||
@ -2,8 +2,8 @@ package com.x8bit.bitwarden.data.platform.manager.network
|
||||
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
@ -19,7 +19,7 @@ class NetworkConfigManagerImpl(
|
||||
authRepository: AuthRepository,
|
||||
environmentRepository: EnvironmentRepository,
|
||||
serverConfigRepository: ServerConfigRepository,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
dispatcherManager: DispatcherManager,
|
||||
) : NetworkConfigManager {
|
||||
|
||||
@ -36,7 +36,6 @@ class NetworkConfigManagerImpl(
|
||||
serverConfigRepository.getServerConfig(forceRefresh = true)
|
||||
}
|
||||
.launchIn(collectionScope)
|
||||
|
||||
refreshAuthenticator.authenticatorProvider = authRepository
|
||||
bitwardenServiceClient.setRefreshTokenProvider(authRepository)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,23 +1,15 @@
|
||||
package com.x8bit.bitwarden.data.vault.datasource.network.di
|
||||
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.CiphersServiceImpl
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.DownloadServiceImpl
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.FolderServiceImpl
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SendsServiceImpl
|
||||
import com.bitwarden.network.service.SyncService
|
||||
import com.bitwarden.network.service.SyncServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.retrofit.Retrofits
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.create
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
@ -30,58 +22,30 @@ object VaultNetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideCiphersService(
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
clock: Clock,
|
||||
): CiphersService = CiphersServiceImpl(
|
||||
azureApi = retrofits
|
||||
.createStaticRetrofit()
|
||||
.create(),
|
||||
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
json = json,
|
||||
clock = clock,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): CiphersService = bitwardenServiceClient.ciphersService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesFolderService(
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
): FolderService = FolderServiceImpl(
|
||||
foldersApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
json = json,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): FolderService = bitwardenServiceClient.folderService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSendsService(
|
||||
retrofits: Retrofits,
|
||||
json: Json,
|
||||
clock: Clock,
|
||||
): SendsService = SendsServiceImpl(
|
||||
azureApi = retrofits
|
||||
.createStaticRetrofit()
|
||||
.create(),
|
||||
sendsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
json = json,
|
||||
clock = clock,
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): SendsService = bitwardenServiceClient.sendsService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncService(
|
||||
retrofits: Retrofits,
|
||||
): SyncService = SyncServiceImpl(
|
||||
syncApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): SyncService = bitwardenServiceClient.syncService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideDownloadService(
|
||||
retrofits: Retrofits,
|
||||
): DownloadService = DownloadServiceImpl(
|
||||
downloadApi = retrofits
|
||||
.createStaticRetrofit()
|
||||
.create(),
|
||||
)
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): DownloadService = bitwardenServiceClient.downloadService
|
||||
}
|
||||
|
||||
@ -3,11 +3,11 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
import android.os.Parcelable
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2GetCredentialsRequest
|
||||
|
||||
@ -2,13 +2,14 @@ package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.bitwarden.data.datasource.disk.model.EnvironmentUrlDataJson
|
||||
import com.bitwarden.network.model.GetTokenResponseJson
|
||||
import com.bitwarden.network.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.model.KdfTypeJson
|
||||
import com.bitwarden.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.bitwarden.network.model.UserDecryptionOptionsJson
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.ForcePasswordResetReason
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkStatic
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
@ -4,15 +4,17 @@ import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.data.manager.DispatcherManager
|
||||
import com.bitwarden.data.repository.ServerConfigRepository
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
|
||||
import com.x8bit.bitwarden.data.util.advanceTimeByAndRunCurrent
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.test.UnconfinedTestDispatcher
|
||||
@ -38,8 +40,9 @@ class NetworkConfigManagerTest {
|
||||
private val serverConfigRepository: ServerConfigRepository = mockk {
|
||||
coEvery { getServerConfig(forceRefresh = true) } returns null
|
||||
}
|
||||
|
||||
private val refreshAuthenticator = RefreshAuthenticator()
|
||||
private val mockBitwardenServiceClient: BitwardenServiceClient = mockk {
|
||||
every { setRefreshTokenProvider(any()) } just runs
|
||||
}
|
||||
|
||||
private lateinit var networkConfigManager: NetworkConfigManager
|
||||
|
||||
@ -49,7 +52,7 @@ class NetworkConfigManagerTest {
|
||||
authRepository = authRepository,
|
||||
environmentRepository = environmentRepository,
|
||||
serverConfigRepository = serverConfigRepository,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
bitwardenServiceClient = mockBitwardenServiceClient,
|
||||
dispatcherManager = dispatcherManager,
|
||||
)
|
||||
}
|
||||
|
||||
@ -3,14 +3,14 @@ package com.x8bit.bitwarden.ui.platform.feature.rootnav
|
||||
import androidx.core.os.bundleOf
|
||||
import com.bitwarden.data.datasource.disk.base.FakeDispatcherManager
|
||||
import com.bitwarden.data.repository.model.Environment
|
||||
import com.bitwarden.network.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.model.OrganizationType
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.auth.datasource.disk.model.OnboardingStatus
|
||||
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.Organization
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.UserState
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.Fido2CreateCredentialRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2CredentialAssertionRequest
|
||||
import com.x8bit.bitwarden.data.autofill.fido2.model.createMockFido2GetCredentialsRequest
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
package com.bitwarden.authenticator.data.auth.datasource.disk
|
||||
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
|
||||
/**
|
||||
* Primary access point for disk information.
|
||||
*/
|
||||
interface AuthDiskSource {
|
||||
interface AuthDiskSource : AppIdProvider {
|
||||
|
||||
/**
|
||||
* Retrieves the "last active time".
|
||||
|
||||
@ -2,10 +2,12 @@ package com.bitwarden.authenticator.data.auth.datasource.disk
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import com.bitwarden.data.datasource.disk.BaseEncryptedDiskSource
|
||||
import java.util.UUID
|
||||
|
||||
private const val AUTHENTICATOR_SYNC_SYMMETRIC_KEY = "authenticatorSyncSymmetricKey"
|
||||
private const val LAST_ACTIVE_TIME_KEY = "lastActiveTime"
|
||||
private const val BIOMETRICS_UNLOCK_KEY = "userKeyBiometricUnlock"
|
||||
private const val UNIQUE_APP_ID_KEY = "appId"
|
||||
|
||||
/**
|
||||
* Primary implementation of [AuthDiskSource].
|
||||
@ -19,6 +21,9 @@ class AuthDiskSourceImpl(
|
||||
),
|
||||
AuthDiskSource {
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = getString(key = UNIQUE_APP_ID_KEY) ?: generateAndStoreUniqueAppId()
|
||||
|
||||
override fun getLastActiveTimeMillis(): Long? =
|
||||
getLong(key = LAST_ACTIVE_TIME_KEY)
|
||||
|
||||
@ -50,4 +55,12 @@ class AuthDiskSourceImpl(
|
||||
}
|
||||
get() = getEncryptedString(AUTHENTICATOR_SYNC_SYMMETRIC_KEY)
|
||||
?.toByteArray(Charsets.ISO_8859_1)
|
||||
|
||||
private fun generateAndStoreUniqueAppId(): String =
|
||||
UUID
|
||||
.randomUUID()
|
||||
.toString()
|
||||
.also {
|
||||
putString(key = UNIQUE_APP_ID_KEY, value = it)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,21 +1,27 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.di
|
||||
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.Retrofits
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.retrofit.RetrofitsImpl
|
||||
import com.bitwarden.authenticator.BuildConfig
|
||||
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_NAME
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_CLIENT_VERSION
|
||||
import com.bitwarden.authenticator.data.platform.datasource.network.util.HEADER_VALUE_USER_AGENT
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.bitwardenServiceClient
|
||||
import com.bitwarden.network.interceptor.AuthTokenProvider
|
||||
import com.bitwarden.network.interceptor.BaseUrlsProvider
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.ConfigServiceImpl
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.create
|
||||
import java.net.Socket
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
import java.security.cert.X509Certificate
|
||||
import java.time.Clock
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@ -28,8 +34,8 @@ object PlatformNetworkModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigService(
|
||||
retrofits: Retrofits,
|
||||
): ConfigService = ConfigServiceImpl(retrofits.unauthenticatedApiRetrofit.create())
|
||||
bitwardenServiceClient: BitwardenServiceClient,
|
||||
): ConfigService = bitwardenServiceClient.configService
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@ -41,21 +47,35 @@ object PlatformNetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesBaseUrlInterceptors(
|
||||
fun provideBitwardenServiceClient(
|
||||
baseUrlsProvider: BaseUrlsProvider,
|
||||
): BaseUrlInterceptors =
|
||||
BaseUrlInterceptors(baseUrlsProvider = baseUrlsProvider)
|
||||
authDiskSource: AuthDiskSource,
|
||||
clock: Clock,
|
||||
): BitwardenServiceClient = bitwardenServiceClient(
|
||||
BitwardenServiceClientConfig(
|
||||
clock = clock,
|
||||
appIdProvider = authDiskSource,
|
||||
clientData = BitwardenServiceClientConfig.ClientData(
|
||||
userAgent = HEADER_VALUE_USER_AGENT,
|
||||
clientName = HEADER_VALUE_CLIENT_NAME,
|
||||
clientVersion = HEADER_VALUE_CLIENT_VERSION,
|
||||
),
|
||||
baseUrlsProvider = baseUrlsProvider,
|
||||
enableHttpBodyLogging = BuildConfig.DEBUG,
|
||||
authTokenProvider = object : AuthTokenProvider {
|
||||
override fun getActiveAccessTokenOrNull(): String? = null
|
||||
},
|
||||
certificateProvider = object : CertificateProvider {
|
||||
override fun chooseClientAlias(
|
||||
keyType: Array<out String>?,
|
||||
issuers: Array<out Principal>?,
|
||||
socket: Socket?,
|
||||
) = ""
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofits(
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
json: Json,
|
||||
): Retrofits =
|
||||
RetrofitsImpl(
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptor,
|
||||
json = json,
|
||||
)
|
||||
override fun getCertificateChain(alias: String?): Array<X509Certificate>? = null
|
||||
|
||||
override fun getPrivateKey(alias: String?): PrivateKey? = null
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,30 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.http.Url
|
||||
|
||||
/**
|
||||
* A collection of various [Retrofit] instances that serve different purposes.
|
||||
*/
|
||||
interface Retrofits {
|
||||
|
||||
/**
|
||||
* Allows access to "/api" calls that do not require authentication.
|
||||
*
|
||||
* The base URL can be dynamically determined via the [BaseUrlInterceptors].
|
||||
*/
|
||||
val unauthenticatedApiRetrofit: Retrofit
|
||||
|
||||
/**
|
||||
* Allows access to static API calls (ex: external APIs).
|
||||
*
|
||||
* @param isAuthenticated Indicates if the [Retrofit] instance should use authentication.
|
||||
* @param baseUrl The static base url associated with this retrofit instance. This can be
|
||||
* overridden with the [Url] annotation.
|
||||
*/
|
||||
fun createStaticRetrofit(
|
||||
isAuthenticated: Boolean = false,
|
||||
baseUrl: String = "https://api.bitwarden.com",
|
||||
): Retrofit
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.bitwarden.network.core.NetworkResultCallAdapterFactory
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
*/
|
||||
class RetrofitsImpl(
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
json: Json,
|
||||
) : Retrofits {
|
||||
|
||||
//region Unauthenticated Retrofits
|
||||
|
||||
override val unauthenticatedApiRetrofit: Retrofit by lazy {
|
||||
createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor = baseUrlInterceptors.apiInterceptor,
|
||||
)
|
||||
}
|
||||
|
||||
//endregion Unauthenticated Retrofits
|
||||
|
||||
//region Static Retrofit
|
||||
|
||||
override fun createStaticRetrofit(isAuthenticated: Boolean, baseUrl: String): Retrofit {
|
||||
return baseRetrofitBuilder
|
||||
.baseUrl(baseUrl)
|
||||
.client(
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
//endregion Static Retrofit
|
||||
|
||||
//region Helper properties and functions
|
||||
|
||||
private val baseOkHttpClient: OkHttpClient =
|
||||
OkHttpClient.Builder()
|
||||
.addInterceptor(headersInterceptor)
|
||||
.build()
|
||||
|
||||
private val baseRetrofit: Retrofit by lazy {
|
||||
baseRetrofitBuilder
|
||||
.baseUrl("https://api.bitwarden.com")
|
||||
.build()
|
||||
}
|
||||
|
||||
private val baseRetrofitBuilder: Retrofit.Builder by lazy {
|
||||
Retrofit.Builder()
|
||||
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
|
||||
.addCallAdapterFactory(NetworkResultCallAdapterFactory())
|
||||
.client(baseOkHttpClient)
|
||||
}
|
||||
|
||||
private fun createUnauthenticatedRetrofit(
|
||||
baseUrlInterceptor: BaseUrlInterceptor,
|
||||
): Retrofit =
|
||||
baseRetrofit
|
||||
.newBuilder()
|
||||
.client(
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.addInterceptor(baseUrlInterceptor)
|
||||
.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
//endregion Helper properties and functions
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
package com.bitwarden.authenticator.data.auth.datasource.disk
|
||||
|
||||
import androidx.core.content.edit
|
||||
import com.bitwarden.authenticatorbridge.util.generateSecretKey
|
||||
import com.bitwarden.data.datasource.disk.base.FakeSharedPreferences
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
@ -37,4 +38,32 @@ class AuthDiskSourceTest {
|
||||
// Retrieving the key from repository should give same byte array despite String conversion:
|
||||
assertTrue(authDiskSource.authenticatorBridgeSymmetricSyncKey.contentEquals(symmetricKey))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uniqueAppId should generate a new ID and update SharedPreferences if none exists`() {
|
||||
val rememberedUniqueAppIdKey = "bwPreferencesStorage:appId"
|
||||
|
||||
// Assert that the SharedPreferences are empty
|
||||
assertNull(fakeSharedPreferences.getString(rememberedUniqueAppIdKey, null))
|
||||
|
||||
// Generate a new uniqueAppId and retrieve it
|
||||
val newId = authDiskSource.uniqueAppId
|
||||
|
||||
// Ensure that the SharedPreferences were updated
|
||||
assertEquals(
|
||||
newId,
|
||||
fakeSharedPreferences.getString(rememberedUniqueAppIdKey, null),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `uniqueAppId should not generate a new ID if one exists`() {
|
||||
val rememberedUniqueAppIdKey = "bwPreferencesStorage:appId"
|
||||
val testId = "testId"
|
||||
|
||||
// Update preferences to hold test value
|
||||
fakeSharedPreferences.edit { putString(rememberedUniqueAppIdKey, testId) }
|
||||
|
||||
assertEquals(testId, authDiskSource.uniqueAppId)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
package com.bitwarden.authenticator.data.auth.datasource.disk.util
|
||||
|
||||
import com.bitwarden.authenticator.data.auth.datasource.disk.AuthDiskSource
|
||||
import java.util.UUID
|
||||
|
||||
class FakeAuthDiskSource : AuthDiskSource {
|
||||
|
||||
private var lastActiveTimeMillis: Long? = null
|
||||
private var userBiometricUnlockKey: String? = null
|
||||
|
||||
override val uniqueAppId: String
|
||||
get() = UUID.randomUUID().toString()
|
||||
|
||||
override fun getLastActiveTimeMillis(): Long? = lastActiveTimeMillis
|
||||
|
||||
override fun storeLastActiveTimeMillis(lastActiveTimeMillis: Long?) {
|
||||
|
||||
@ -1,127 +0,0 @@
|
||||
package com.bitwarden.authenticator.data.platform.datasource.network.retrofit
|
||||
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.slot
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import retrofit2.http.GET
|
||||
|
||||
class RetrofitsTest {
|
||||
private val baseUrlInterceptors = mockk<BaseUrlInterceptors> {
|
||||
every { apiInterceptor } returns mockk {
|
||||
mockIntercept { isApiInterceptorCalled = true }
|
||||
}
|
||||
}
|
||||
private val headersInterceptors = mockk<HeadersInterceptor> {
|
||||
mockIntercept { isheadersInterceptorCalled = true }
|
||||
}
|
||||
private val json = Json
|
||||
private val server = MockWebServer()
|
||||
|
||||
private val retrofits = RetrofitsImpl(
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptors,
|
||||
json = json,
|
||||
)
|
||||
|
||||
private var isAuthInterceptorCalled = false
|
||||
private var isApiInterceptorCalled = false
|
||||
private var isheadersInterceptorCalled = false
|
||||
private var isRefreshAuthenticatorCalled = false
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
server.start()
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unauthenticatedApiRetrofit should not invoke the RefreshAuthenticator`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.unauthenticatedApiRetrofit
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setResponseCode(401).setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isRefreshAuthenticatorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unauthenticatedApiRetrofit should invoke the correct interceptors`() = runBlocking {
|
||||
val testApi = retrofits
|
||||
.unauthenticatedApiRetrofit
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertTrue(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createStaticRetrofit when unauthenticated should invoke the correct interceptors`() =
|
||||
runBlocking {
|
||||
val testApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = false)
|
||||
.createMockRetrofit()
|
||||
.create<TestApi>()
|
||||
|
||||
server.enqueue(MockResponse().setBody("""{}"""))
|
||||
|
||||
testApi.test()
|
||||
|
||||
assertFalse(isAuthInterceptorCalled)
|
||||
assertFalse(isApiInterceptorCalled)
|
||||
assertTrue(isheadersInterceptorCalled)
|
||||
}
|
||||
|
||||
private fun Retrofit.createMockRetrofit(): Retrofit =
|
||||
this
|
||||
.newBuilder()
|
||||
.baseUrl(server.url("/").toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
interface TestApi {
|
||||
@GET("/test")
|
||||
suspend fun test(): NetworkResult<JsonObject>
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks the given [Interceptor] such that the [Interceptor.intercept] is a no-op but triggers the
|
||||
* [isCalledCallback].
|
||||
*/
|
||||
private fun Interceptor.mockIntercept(isCalledCallback: () -> Unit) {
|
||||
val chainSlot = slot<Interceptor.Chain>()
|
||||
every { intercept(capture(chainSlot)) } answers {
|
||||
isCalledCallback()
|
||||
val chain = chainSlot.captured
|
||||
chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
@ -65,6 +65,8 @@ kover {
|
||||
"androidx.compose.ui.tooling.preview.PreviewScreenSizes",
|
||||
// Manually excluded classes/files/etc.
|
||||
"com.bitwarden.core.annotation.OmitFromCoverage",
|
||||
// Dagger modules
|
||||
"dagger.Module",
|
||||
)
|
||||
classes(
|
||||
// Navigation helpers
|
||||
|
||||
@ -2,7 +2,6 @@ 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)
|
||||
@ -48,8 +47,6 @@ 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)
|
||||
|
||||
@ -0,0 +1,162 @@
|
||||
@file:OmitFromCoverage
|
||||
|
||||
package com.bitwarden.network
|
||||
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.bitwarden.network.service.AccountsService
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.EventService
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedService
|
||||
import com.bitwarden.network.service.IdentityService
|
||||
import com.bitwarden.network.service.NewAuthRequestService
|
||||
import com.bitwarden.network.service.OrganizationService
|
||||
import com.bitwarden.network.service.PushService
|
||||
import com.bitwarden.network.service.SendsService
|
||||
import com.bitwarden.network.service.SyncService
|
||||
|
||||
/**
|
||||
* Provides access to Bitwarden services.
|
||||
*
|
||||
* New instances of this class should be created using the [bitwardenServiceClient] factory
|
||||
* function.
|
||||
*
|
||||
* Example initialization:
|
||||
* ```
|
||||
* val bitwardenServiceClient = bitwardenServiceClient(
|
||||
* BitwardenServiceClientConfig(
|
||||
* clock = clock,
|
||||
* json = json,
|
||||
* appIdProvider = appIdProvider,
|
||||
* clientData = BitwardenServiceClientConfig.ClientData(
|
||||
* userAgent = "my-user-agent-string",
|
||||
* clientName = "my-application",
|
||||
* clientVersion = "versionName",
|
||||
* ),
|
||||
* authTokenProvider = authTokenProvider,
|
||||
* baseUrlsProvider = baseUrlsProvider,
|
||||
* certificateProvider = certificateProvider,
|
||||
* ),
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
interface BitwardenServiceClient {
|
||||
|
||||
/**
|
||||
* Provides access to the Accounts service.
|
||||
*/
|
||||
val accountsService: AccountsService
|
||||
|
||||
/**
|
||||
* Provides access to the Authentication Requests service.
|
||||
*/
|
||||
val authRequestsService: AuthRequestsService
|
||||
|
||||
/**
|
||||
* Provides access to the Ciphers service.
|
||||
*/
|
||||
val ciphersService: CiphersService
|
||||
|
||||
/**
|
||||
* Provides access to the Configuration service.
|
||||
*/
|
||||
val configService: ConfigService
|
||||
|
||||
/**
|
||||
* Provides access to the Digital Asset Link service.
|
||||
*/
|
||||
val digitalAssetLinkService: DigitalAssetLinkService
|
||||
|
||||
/**
|
||||
* Provides access to the Devices service.
|
||||
*/
|
||||
val devicesService: DevicesService
|
||||
|
||||
/**
|
||||
* Provides access to the Download service.
|
||||
*/
|
||||
val downloadService: DownloadService
|
||||
|
||||
/**
|
||||
* Provides access to the Event service.
|
||||
*/
|
||||
val eventService: EventService
|
||||
|
||||
/**
|
||||
* Provides access to the Folder service.
|
||||
*/
|
||||
val folderService: FolderService
|
||||
|
||||
/**
|
||||
* Provides access to the Have I Been Pwned service.
|
||||
*/
|
||||
val haveIBeenPwnedService: HaveIBeenPwnedService
|
||||
|
||||
/**
|
||||
* Provides access to the Identity service.
|
||||
*/
|
||||
val identityService: IdentityService
|
||||
|
||||
/**
|
||||
* Provides access to the New Authentication Request service.
|
||||
*/
|
||||
val newAuthRequestService: NewAuthRequestService
|
||||
|
||||
/**
|
||||
* Provides access to the Organization service.
|
||||
*/
|
||||
val organizationService: OrganizationService
|
||||
|
||||
/**
|
||||
* Provides access to the Push service.
|
||||
*/
|
||||
val pushService: PushService
|
||||
|
||||
/**
|
||||
* Provides access to the Sync service.
|
||||
*/
|
||||
val syncService: SyncService
|
||||
|
||||
/**
|
||||
* Provides access to the Sends service.
|
||||
*/
|
||||
val sendsService: SendsService
|
||||
|
||||
/**
|
||||
* Sets the [refreshTokenProvider] to be used for refreshing access tokens.
|
||||
*/
|
||||
fun setRefreshTokenProvider(refreshTokenProvider: RefreshTokenProvider?)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [BitwardenServiceClient] with the given [config].
|
||||
*
|
||||
* Example initialization:
|
||||
* ```
|
||||
* val bitwardenServiceClient = bitwardenServiceClient(
|
||||
* BitwardenServiceClientConfig(
|
||||
* clock = clock,
|
||||
* json = json,
|
||||
* appIdProvider = appIdProvider,
|
||||
* clientData = BitwardenServiceClientConfig.ClientData(
|
||||
* userAgent = "my-user-agent-string",
|
||||
* clientName = "my-application",
|
||||
* clientVersion = "versionName",
|
||||
* ),
|
||||
* authTokenProvider = authTokenProvider,
|
||||
* baseUrlsProvider = baseUrlsProvider,
|
||||
* certificateProvider = certificateProvider,
|
||||
* ),
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
fun bitwardenServiceClient(
|
||||
config: BitwardenServiceClientConfig,
|
||||
): BitwardenServiceClient = BitwardenServiceClientImpl(config)
|
||||
@ -0,0 +1,207 @@
|
||||
package com.bitwarden.network
|
||||
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.core.data.serializer.ZonedDateTimeSerializer
|
||||
import com.bitwarden.network.authenticator.RefreshAuthenticator
|
||||
import com.bitwarden.network.interceptor.AuthTokenInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.BitwardenServiceClientConfig
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.bitwarden.network.retrofit.Retrofits
|
||||
import com.bitwarden.network.retrofit.RetrofitsImpl
|
||||
import com.bitwarden.network.service.AccountsServiceImpl
|
||||
import com.bitwarden.network.service.AuthRequestsService
|
||||
import com.bitwarden.network.service.AuthRequestsServiceImpl
|
||||
import com.bitwarden.network.service.CiphersService
|
||||
import com.bitwarden.network.service.CiphersServiceImpl
|
||||
import com.bitwarden.network.service.ConfigService
|
||||
import com.bitwarden.network.service.ConfigServiceImpl
|
||||
import com.bitwarden.network.service.DevicesService
|
||||
import com.bitwarden.network.service.DevicesServiceImpl
|
||||
import com.bitwarden.network.service.DigitalAssetLinkService
|
||||
import com.bitwarden.network.service.DigitalAssetLinkServiceImpl
|
||||
import com.bitwarden.network.service.DownloadService
|
||||
import com.bitwarden.network.service.DownloadServiceImpl
|
||||
import com.bitwarden.network.service.EventService
|
||||
import com.bitwarden.network.service.EventServiceImpl
|
||||
import com.bitwarden.network.service.FolderService
|
||||
import com.bitwarden.network.service.FolderServiceImpl
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedService
|
||||
import com.bitwarden.network.service.HaveIBeenPwnedServiceImpl
|
||||
import com.bitwarden.network.service.IdentityService
|
||||
import com.bitwarden.network.service.IdentityServiceImpl
|
||||
import com.bitwarden.network.service.NewAuthRequestService
|
||||
import com.bitwarden.network.service.NewAuthRequestServiceImpl
|
||||
import com.bitwarden.network.service.OrganizationService
|
||||
import com.bitwarden.network.service.OrganizationServiceImpl
|
||||
import com.bitwarden.network.service.PushService
|
||||
import com.bitwarden.network.service.PushServiceImpl
|
||||
import com.bitwarden.network.service.SendsServiceImpl
|
||||
import com.bitwarden.network.service.SyncServiceImpl
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.contextual
|
||||
import retrofit2.create
|
||||
|
||||
/**
|
||||
* Primary implementation of [BitwardenServiceClient].
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
internal class BitwardenServiceClientImpl(
|
||||
private val bitwardenServiceClientConfig: BitwardenServiceClientConfig,
|
||||
) : BitwardenServiceClient {
|
||||
|
||||
private val refreshAuthenticator: RefreshAuthenticator = RefreshAuthenticator()
|
||||
private val clientJson = Json {
|
||||
|
||||
// If there are keys returned by the server not modeled by a serializable class,
|
||||
// ignore them.
|
||||
// This makes additive server changes non-breaking.
|
||||
ignoreUnknownKeys = true
|
||||
|
||||
// We allow for nullable values to have keys missing in the JSON response.
|
||||
explicitNulls = false
|
||||
serializersModule = SerializersModule {
|
||||
contextual(ZonedDateTimeSerializer())
|
||||
}
|
||||
|
||||
// Respect model default property values.
|
||||
coerceInputValues = true
|
||||
}
|
||||
private val retrofits: Retrofits by lazy {
|
||||
RetrofitsImpl(
|
||||
authTokenInterceptor = AuthTokenInterceptor(
|
||||
authTokenProvider = bitwardenServiceClientConfig.authTokenProvider,
|
||||
),
|
||||
baseUrlInterceptors = BaseUrlInterceptors(
|
||||
baseUrlsProvider = bitwardenServiceClientConfig.baseUrlsProvider,
|
||||
),
|
||||
headersInterceptor = HeadersInterceptor(
|
||||
userAgent = bitwardenServiceClientConfig.clientData.userAgent,
|
||||
clientName = bitwardenServiceClientConfig.clientData.clientName,
|
||||
clientVersion = bitwardenServiceClientConfig.clientData.clientVersion,
|
||||
),
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
logHttpBody = bitwardenServiceClientConfig.enableHttpBodyLogging,
|
||||
certificateProvider = bitwardenServiceClientConfig.certificateProvider,
|
||||
json = clientJson,
|
||||
)
|
||||
}
|
||||
|
||||
override val accountsService by lazy {
|
||||
AccountsServiceImpl(
|
||||
unauthenticatedAccountsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
authenticatedAccountsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedKeyConnectorApi = retrofits.createStaticRetrofit().create(),
|
||||
authenticatedKeyConnectorApi = retrofits
|
||||
.createStaticRetrofit(isAuthenticated = true)
|
||||
.create(),
|
||||
json = clientJson,
|
||||
)
|
||||
}
|
||||
|
||||
override val authRequestsService: AuthRequestsService by lazy {
|
||||
AuthRequestsServiceImpl(
|
||||
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val ciphersService: CiphersService by lazy {
|
||||
CiphersServiceImpl(
|
||||
azureApi = retrofits.createStaticRetrofit().create(),
|
||||
ciphersApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
json = clientJson,
|
||||
clock = bitwardenServiceClientConfig.clock,
|
||||
)
|
||||
}
|
||||
|
||||
override val configService: ConfigService by lazy {
|
||||
ConfigServiceImpl(
|
||||
configApi = retrofits.createStaticRetrofit().create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val devicesService: DevicesService by lazy {
|
||||
DevicesServiceImpl(
|
||||
authenticatedDevicesApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedDevicesApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val digitalAssetLinkService: DigitalAssetLinkService by lazy {
|
||||
DigitalAssetLinkServiceImpl(
|
||||
digitalAssetLinkApi = retrofits.createStaticRetrofit().create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val downloadService: DownloadService by lazy {
|
||||
DownloadServiceImpl(downloadApi = retrofits.createStaticRetrofit().create())
|
||||
}
|
||||
|
||||
override val eventService: EventService by lazy {
|
||||
EventServiceImpl(eventApi = retrofits.authenticatedApiRetrofit.create())
|
||||
}
|
||||
|
||||
override val folderService: FolderService by lazy {
|
||||
FolderServiceImpl(
|
||||
foldersApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
json = clientJson,
|
||||
)
|
||||
}
|
||||
|
||||
override val haveIBeenPwnedService: HaveIBeenPwnedService by lazy {
|
||||
HaveIBeenPwnedServiceImpl(
|
||||
api = retrofits
|
||||
.createStaticRetrofit(baseUrl = "https://api.pwnedpasswords.com")
|
||||
.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val identityService: IdentityService by lazy {
|
||||
IdentityServiceImpl(
|
||||
unauthenticatedIdentityApi = retrofits.unauthenticatedIdentityRetrofit.create(),
|
||||
json = clientJson,
|
||||
)
|
||||
}
|
||||
|
||||
override val newAuthRequestService: NewAuthRequestService by lazy {
|
||||
NewAuthRequestServiceImpl(
|
||||
authenticatedAuthRequestsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedAuthRequestsApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val organizationService: OrganizationService by lazy {
|
||||
OrganizationServiceImpl(
|
||||
authenticatedOrganizationApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
unauthenticatedOrganizationApi = retrofits.unauthenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override val pushService: PushService by lazy {
|
||||
PushServiceImpl(
|
||||
pushApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
appId = bitwardenServiceClientConfig.appIdProvider.uniqueAppId,
|
||||
)
|
||||
}
|
||||
|
||||
override val sendsService by lazy {
|
||||
SendsServiceImpl(
|
||||
sendsApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
azureApi = retrofits.createStaticRetrofit().create(),
|
||||
json = clientJson,
|
||||
clock = bitwardenServiceClientConfig.clock,
|
||||
)
|
||||
}
|
||||
|
||||
override val syncService by lazy {
|
||||
SyncServiceImpl(
|
||||
syncApi = retrofits.authenticatedApiRetrofit.create(),
|
||||
)
|
||||
}
|
||||
|
||||
override fun setRefreshTokenProvider(refreshTokenProvider: RefreshTokenProvider?) {
|
||||
refreshAuthenticator.refreshTokenProvider = refreshTokenProvider
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
package com.bitwarden.network.authenticator
|
||||
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.bitwarden.network.util.HEADER_KEY_AUTHORIZATION
|
||||
import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import okhttp3.Authenticator
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.Route
|
||||
|
||||
/**
|
||||
* An authenticator used to refresh the access token when a 401 is returned from an API. Upon
|
||||
* successfully getting a new access token, the original request is retried.
|
||||
*/
|
||||
internal class RefreshAuthenticator : Authenticator {
|
||||
|
||||
var refreshTokenProvider: RefreshTokenProvider? = null
|
||||
|
||||
override fun authenticate(
|
||||
route: Route?,
|
||||
response: Response,
|
||||
): Request? {
|
||||
val accessToken = requireNotNull(
|
||||
response
|
||||
.request
|
||||
.header(name = HEADER_KEY_AUTHORIZATION)
|
||||
?.substringAfter(delimiter = HEADER_VALUE_BEARER_PREFIX),
|
||||
)
|
||||
return when (val userId = parseJwtTokenDataOrNull(accessToken)?.userId) {
|
||||
null -> {
|
||||
// We are unable to get the user ID, let's just let the 401 pass through.
|
||||
null
|
||||
}
|
||||
|
||||
else -> {
|
||||
refreshTokenProvider
|
||||
?.refreshAccessTokenSynchronously(userId = userId)
|
||||
?.fold(
|
||||
onFailure = { null },
|
||||
onSuccess = { newAccessToken ->
|
||||
response.request
|
||||
.newBuilder()
|
||||
.header(
|
||||
name = HEADER_KEY_AUTHORIZATION,
|
||||
value = HEADER_VALUE_BEARER_PREFIX + newAccessToken.accessToken,
|
||||
)
|
||||
.build()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -5,12 +5,10 @@ import com.bitwarden.network.util.HEADER_VALUE_BEARER_PREFIX
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import java.io.IOException
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Interceptor responsible for adding the auth token(Bearer) to API requests.
|
||||
*/
|
||||
@Singleton
|
||||
class AuthTokenInterceptor(
|
||||
private val authTokenProvider: AuthTokenProvider,
|
||||
) : Interceptor {
|
||||
|
||||
@ -1,15 +1,12 @@
|
||||
package com.bitwarden.network.interceptor
|
||||
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* An overall container for various [BaseUrlInterceptor] implementations for different API groups.
|
||||
*/
|
||||
@OmitFromCoverage
|
||||
@Singleton
|
||||
class BaseUrlInterceptors @Inject constructor(
|
||||
class BaseUrlInterceptors(
|
||||
private val baseUrlsProvider: BaseUrlsProvider,
|
||||
) {
|
||||
/**
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import com.bitwarden.network.BitwardenServiceClient
|
||||
import com.bitwarden.network.interceptor.AuthTokenProvider
|
||||
import com.bitwarden.network.interceptor.BaseUrlsProvider
|
||||
import com.bitwarden.network.provider.AppIdProvider
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import java.time.Clock
|
||||
|
||||
/**
|
||||
* Models configuration for [BitwardenServiceClient].
|
||||
*/
|
||||
data class BitwardenServiceClientConfig(
|
||||
val clientData: ClientData,
|
||||
val appIdProvider: AppIdProvider,
|
||||
val baseUrlsProvider: BaseUrlsProvider,
|
||||
val authTokenProvider: AuthTokenProvider,
|
||||
val certificateProvider: CertificateProvider,
|
||||
val clock: Clock = Clock.systemDefaultZone(),
|
||||
val enableHttpBodyLogging: Boolean = false,
|
||||
) {
|
||||
/**
|
||||
* Models data about the client application.
|
||||
*/
|
||||
data class ClientData(
|
||||
val userAgent: String,
|
||||
val clientName: String,
|
||||
val clientVersion: String,
|
||||
)
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.model
|
||||
package com.bitwarden.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -0,0 +1,12 @@
|
||||
package com.bitwarden.network.provider
|
||||
|
||||
/**
|
||||
* A provider for all the functionality needed to uniquely identify the app installation.
|
||||
*/
|
||||
interface AppIdProvider {
|
||||
|
||||
/**
|
||||
* The unique app ID.
|
||||
*/
|
||||
val uniqueAppId: String
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
package com.bitwarden.network.provider
|
||||
|
||||
import com.bitwarden.network.model.RefreshTokenResponseJson
|
||||
|
||||
/**
|
||||
* A provider for all the functionality needed to refresh a user's access token.
|
||||
*/
|
||||
interface RefreshTokenProvider {
|
||||
/**
|
||||
* Attempt to refresh the user's access token based on the [userId].
|
||||
*
|
||||
* This call is both synchronous and performs a network request. Make sure that you are calling
|
||||
* from an appropriate thread.
|
||||
*/
|
||||
fun refreshAccessTokenSynchronously(userId: String): Result<RefreshTokenResponseJson>
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
package com.bitwarden.network.retrofit
|
||||
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import retrofit2.Retrofit
|
||||
@ -7,7 +7,7 @@ import retrofit2.http.Url
|
||||
/**
|
||||
* A collection of various [Retrofit] instances that serve different purposes.
|
||||
*/
|
||||
interface Retrofits {
|
||||
internal interface Retrofits {
|
||||
/**
|
||||
* Allows access to "/api" calls that must be authenticated.
|
||||
*
|
||||
@ -1,15 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
package com.bitwarden.network.retrofit
|
||||
|
||||
import com.bitwarden.network.authenticator.RefreshAuthenticator
|
||||
import com.bitwarden.network.core.NetworkResultCallAdapterFactory
|
||||
import com.bitwarden.network.interceptor.AuthTokenInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.ssl.BitwardenX509ExtendedKeyManager
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
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.ssl.BitwardenX509ExtendedKeyManager
|
||||
import com.x8bit.bitwarden.data.platform.util.isDevBuild
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
@ -21,19 +20,20 @@ import java.security.KeyStore
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.X509ExtendedKeyManager
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* Primary implementation of [Retrofits].
|
||||
*/
|
||||
class RetrofitsImpl(
|
||||
@Suppress("LongParameterList")
|
||||
internal class RetrofitsImpl(
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
baseUrlInterceptors: BaseUrlInterceptors,
|
||||
headersInterceptor: HeadersInterceptor,
|
||||
refreshAuthenticator: RefreshAuthenticator,
|
||||
refreshAuthenticator: RefreshAuthenticator?,
|
||||
json: Json,
|
||||
private val certificateProvider: CertificateProvider,
|
||||
private val logHttpBody: Boolean = false,
|
||||
) : Retrofits {
|
||||
//region Authenticated Retrofits
|
||||
|
||||
@ -91,22 +91,24 @@ class RetrofitsImpl(
|
||||
redactHeader(name = HEADER_KEY_AUTHORIZATION)
|
||||
setLevel(
|
||||
level = HttpLoggingInterceptor.Level.BODY
|
||||
.takeIf { isDevBuild }
|
||||
.takeIf { logHttpBody }
|
||||
?: HttpLoggingInterceptor.Level.BASIC,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val baseOkHttpClient: OkHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(headersInterceptor)
|
||||
.setSslSocketFactory()
|
||||
.build()
|
||||
.addInterceptor(headersInterceptor)
|
||||
.configureSsl()
|
||||
.build()
|
||||
|
||||
private val authenticatedOkHttpClient: OkHttpClient by lazy {
|
||||
baseOkHttpClient
|
||||
.newBuilder()
|
||||
.authenticator(refreshAuthenticator)
|
||||
.addInterceptor(authTokenInterceptor)
|
||||
.also { builder ->
|
||||
refreshAuthenticator?.let { builder.authenticator(it) }
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
@ -157,22 +159,20 @@ class RetrofitsImpl(
|
||||
.apply { init(null as KeyStore?) }
|
||||
.trustManagers
|
||||
|
||||
private fun createX509KeyManager(): X509ExtendedKeyManager =
|
||||
BitwardenX509ExtendedKeyManager(certificateProvider = certificateProvider)
|
||||
|
||||
private fun createSslContext(): SSLContext = SSLContext
|
||||
.getInstance("TLS")
|
||||
.apply {
|
||||
private fun createSslContext(certificateProvider: CertificateProvider): SSLContext = SSLContext
|
||||
.getInstance("TLS").apply {
|
||||
init(
|
||||
arrayOf(createX509KeyManager()),
|
||||
arrayOf(
|
||||
BitwardenX509ExtendedKeyManager(certificateProvider = certificateProvider),
|
||||
),
|
||||
createSslTrustManagers(),
|
||||
null,
|
||||
)
|
||||
}
|
||||
|
||||
private fun OkHttpClient.Builder.setSslSocketFactory(): OkHttpClient.Builder =
|
||||
private fun OkHttpClient.Builder.configureSsl(): OkHttpClient.Builder =
|
||||
sslSocketFactory(
|
||||
createSslContext().socketFactory,
|
||||
createSslContext(certificateProvider = certificateProvider).socketFactory,
|
||||
createSslTrustManagers().first() as X509TrustManager,
|
||||
)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.ssl
|
||||
package com.bitwarden.network.ssl
|
||||
|
||||
import com.bitwarden.core.annotation.OmitFromCoverage
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import java.net.Socket
|
||||
import java.security.Principal
|
||||
import java.security.PrivateKey
|
||||
@ -1,7 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.auth.repository.util
|
||||
package com.bitwarden.network.util
|
||||
|
||||
import com.bitwarden.network.util.base64UrlDecodeOrNull
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.model.JwtTokenDataJson
|
||||
import kotlinx.serialization.json.Json
|
||||
import timber.log.Timber
|
||||
|
||||
@ -1,16 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.authenticator
|
||||
package com.bitwarden.network.authenticator
|
||||
|
||||
import com.bitwarden.core.data.util.asFailure
|
||||
import com.bitwarden.core.data.util.asSuccess
|
||||
import com.bitwarden.network.model.JwtTokenDataJson
|
||||
import com.bitwarden.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.JwtTokenDataJson
|
||||
import com.x8bit.bitwarden.data.auth.repository.model.LogoutReason
|
||||
import com.x8bit.bitwarden.data.auth.repository.util.parseJwtTokenDataOrNull
|
||||
import com.bitwarden.network.provider.RefreshTokenProvider
|
||||
import com.bitwarden.network.util.parseJwtTokenDataOrNull
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.runs
|
||||
import io.mockk.unmockkStatic
|
||||
import io.mockk.verify
|
||||
import okhttp3.Protocol
|
||||
@ -24,12 +22,12 @@ import org.junit.jupiter.api.Test
|
||||
|
||||
class RefreshAuthenticatorTests {
|
||||
private lateinit var authenticator: RefreshAuthenticator
|
||||
private val authenticatorProvider: AuthenticatorProvider = mockk()
|
||||
private val refreshTokenProvider: RefreshTokenProvider = mockk()
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
authenticator = RefreshAuthenticator()
|
||||
authenticator.authenticatorProvider = authenticatorProvider
|
||||
authenticator.refreshTokenProvider = refreshTokenProvider
|
||||
|
||||
mockkStatic(::parseJwtTokenDataOrNull)
|
||||
}
|
||||
@ -39,18 +37,6 @@ class RefreshAuthenticatorTests {
|
||||
unmockkStatic(::parseJwtTokenDataOrNull)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshAuthenticator returns null if the request is for a different user`() {
|
||||
every { parseJwtTokenDataOrNull(JWT_ACCESS_TOKEN) } returns JTW_TOKEN
|
||||
every { authenticatorProvider.activeUserId } returns "different_user_id"
|
||||
|
||||
assertNull(authenticator.authenticate(null, RESPONSE_401))
|
||||
|
||||
verify(exactly = 1) {
|
||||
authenticatorProvider.activeUserId
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshAuthenticator returns null if API has no authorization user ID`() {
|
||||
every { parseJwtTokenDataOrNull(JWT_ACCESS_TOKEN) } returns null
|
||||
@ -58,39 +44,37 @@ class RefreshAuthenticatorTests {
|
||||
assertNull(authenticator.authenticate(null, RESPONSE_401))
|
||||
|
||||
verify(exactly = 0) {
|
||||
authenticatorProvider.activeUserId
|
||||
authenticatorProvider.refreshAccessTokenSynchronously(any())
|
||||
authenticatorProvider.logout(userId = any(), reason = LogoutReason.TokenRefreshFail)
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `RefreshAuthenticator returns null and logs out when request is for active user and refresh is failure`() {
|
||||
fun `RefreshAuthenticator returns null when refresh is failure`() {
|
||||
every { parseJwtTokenDataOrNull(JWT_ACCESS_TOKEN) } returns JTW_TOKEN
|
||||
every { authenticatorProvider.activeUserId } returns USER_ID
|
||||
every {
|
||||
authenticatorProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
} returns Throwable("Fail").asFailure()
|
||||
every {
|
||||
authenticatorProvider.logout(
|
||||
userId = USER_ID,
|
||||
reason = LogoutReason.TokenRefreshFail,
|
||||
)
|
||||
} just runs
|
||||
|
||||
assertNull(authenticator.authenticate(null, RESPONSE_401))
|
||||
|
||||
verify(exactly = 1) {
|
||||
authenticatorProvider.activeUserId
|
||||
authenticatorProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
authenticatorProvider.logout(userId = USER_ID, reason = LogoutReason.TokenRefreshFail)
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `RefreshAuthenticator returns null when refreshTokenProvider is null`() {
|
||||
authenticator.refreshTokenProvider = null
|
||||
every { parseJwtTokenDataOrNull(JWT_ACCESS_TOKEN) } returns JTW_TOKEN
|
||||
assertNull(authenticator.authenticate(null, RESPONSE_401))
|
||||
verify(exactly = 0) {
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(any())
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `RefreshAuthenticator returns updated request when request is for active user and refresh is success`() {
|
||||
fun `RefreshAuthenticator returns updated request when refresh is success`() {
|
||||
val newAccessToken = "newAccessToken"
|
||||
val refreshResponse = RefreshTokenResponseJson(
|
||||
accessToken = newAccessToken,
|
||||
@ -99,9 +83,8 @@ class RefreshAuthenticatorTests {
|
||||
tokenType = "Bearer",
|
||||
)
|
||||
every { parseJwtTokenDataOrNull(JWT_ACCESS_TOKEN) } returns JTW_TOKEN
|
||||
every { authenticatorProvider.activeUserId } returns USER_ID
|
||||
every {
|
||||
authenticatorProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
} returns refreshResponse.asSuccess()
|
||||
|
||||
val authenticatedRequest = authenticator.authenticate(null, RESPONSE_401)
|
||||
@ -113,8 +96,7 @@ class RefreshAuthenticatorTests {
|
||||
authenticatedRequest!!.header("Authorization"),
|
||||
)
|
||||
verify(exactly = 1) {
|
||||
authenticatorProvider.activeUserId
|
||||
authenticatorProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
refreshTokenProvider.refreshAccessTokenSynchronously(USER_ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,7 @@ import okhttp3.Request
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Test
|
||||
import java.io.IOException
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthTokenInterceptorTest {
|
||||
private val mockAuthTokenProvider = mockk<AuthTokenProvider> {
|
||||
every { getActiveAccessTokenOrNull() } returns null
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.retrofit
|
||||
package com.bitwarden.network.retrofit
|
||||
|
||||
import com.bitwarden.network.authenticator.RefreshAuthenticator
|
||||
import com.bitwarden.network.interceptor.AuthTokenInterceptor
|
||||
import com.bitwarden.network.interceptor.BaseUrlInterceptors
|
||||
import com.bitwarden.network.interceptor.HeadersInterceptor
|
||||
import com.bitwarden.network.model.NetworkResult
|
||||
import com.bitwarden.network.ssl.CertificateProvider
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.authenticator.RefreshAuthenticator
|
||||
import io.mockk.MockKMatcherScope
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import io.mockk.mockkConstructor
|
||||
import io.mockk.slot
|
||||
import io.mockk.unmockkConstructor
|
||||
import io.mockk.verify
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import okhttp3.Authenticator
|
||||
@ -266,6 +270,32 @@ class RetrofitsTest {
|
||||
assertFalse(isEventsInterceptorCalled)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `createStaticRetrofit should set sslSocketFactory when certificateProvider is not null`() =
|
||||
runTest {
|
||||
mockkConstructor(OkHttpClient.Builder::class)
|
||||
mockBuilder<OkHttpClient.Builder> {
|
||||
it.addInterceptor(baseUrlInterceptors.apiInterceptor)
|
||||
}
|
||||
every {
|
||||
anyConstructed<OkHttpClient.Builder>().sslSocketFactory(any(), any())
|
||||
} returns mockk(relaxed = true)
|
||||
val retrofits = RetrofitsImpl(
|
||||
authTokenInterceptor = authTokenInterceptor,
|
||||
baseUrlInterceptors = baseUrlInterceptors,
|
||||
headersInterceptor = headersInterceptors,
|
||||
refreshAuthenticator = refreshAuthenticator,
|
||||
certificateProvider = certificateProvider,
|
||||
json = json,
|
||||
)
|
||||
|
||||
retrofits.createStaticRetrofit()
|
||||
|
||||
verify(exactly = 1) {
|
||||
anyConstructed<OkHttpClient.Builder>().sslSocketFactory(any(), any())
|
||||
}
|
||||
}
|
||||
|
||||
private fun Retrofit.createMockRetrofit(): Retrofit =
|
||||
this
|
||||
.newBuilder()
|
||||
@ -301,3 +331,31 @@ private fun Interceptor.mockIntercept(isCalledCallback: () -> Unit) {
|
||||
chain.proceed(chain.request())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method for mocking pipeline operations within the builder pattern. This saves a lot of
|
||||
* boiler plate. In order to use this, the builder's constructor must be mockked.
|
||||
*
|
||||
* Example:
|
||||
* ```
|
||||
* // Setup
|
||||
* mockkConstructor(FillResponse.Builder::class)
|
||||
* mockBuilder<FillResponse.Builder> { it.setIgnoredIds() }
|
||||
* every { anyConstructed<FillResponse.Builder>().build() } returns mockk()
|
||||
*
|
||||
* // Test
|
||||
* ...
|
||||
*
|
||||
* // Verify
|
||||
* verify(exactly = 1) {
|
||||
* anyConstructed<FillResponse.Builder>().setIgnoredIds()
|
||||
* anyConstructed<FillResponse.Builder>().build()
|
||||
* }
|
||||
* unmockkConstructor(FillResponse.Builder::class)
|
||||
* ```
|
||||
*/
|
||||
inline fun <reified T : Any> mockBuilder(crossinline block: MockKMatcherScope.(T) -> T) {
|
||||
every { block(anyConstructed<T>()) } answers {
|
||||
this.self as T
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user