BIT-405: AuthTokenInterceptor (#57)

This commit is contained in:
Ramsey Smith 2023-09-21 11:35:32 -06:00 committed by Álison Fernandes
parent 016f597d8c
commit 046a94939e
5 changed files with 198 additions and 22 deletions

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthState
import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult import com.x8bit.bitwarden.data.auth.datasource.network.model.LoginResult
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import com.x8bit.bitwarden.data.platform.util.flatMap import com.x8bit.bitwarden.data.platform.util.flatMap
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
@ -22,6 +23,7 @@ class AuthRepositoryImpl @Inject constructor(
private val accountsApi: AccountsApi, private val accountsApi: AccountsApi,
private val identityApi: IdentityApi, private val identityApi: IdentityApi,
private val bitwardenSdkClient: Client, private val bitwardenSdkClient: Client,
private val authTokenInterceptor: AuthTokenInterceptor,
) : AuthRepository { ) : AuthRepository {
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Unauthenticated) private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
@ -55,6 +57,8 @@ class AuthRepositoryImpl @Inject constructor(
LoginResult.Error LoginResult.Error
}, },
onSuccess = { onSuccess = {
// TODO: Create intermediate class for providing auth token to interceptor (BIT-411)
authTokenInterceptor.authToken = it.accessToken
mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken) mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken)
LoginResult.Success LoginResult.Success
}, },

View file

@ -5,6 +5,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi import com.x8bit.bitwarden.data.platform.datasource.network.api.ConfigApi
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCallAdapterFactory
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
@ -15,6 +16,7 @@ import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit import retrofit2.Retrofit
import retrofit2.create import retrofit2.create
import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
/** /**
@ -25,45 +27,74 @@ import javax.inject.Singleton
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NetworkModule { object NetworkModule {
@Provides private const val AUTHORIZED = "authorized"
@Singleton private const val UNAUTHORIZED = "unauthorized"
fun providesAccountsApiService(retrofit: Retrofit): AccountsApi = retrofit.create()
@Provides @Provides
@Singleton @Singleton
fun providesConfigApiService(retrofit: Retrofit): ConfigApi = retrofit.create() fun providesAccountsApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): AccountsApi =
retrofit.create()
@Provides @Provides
@Singleton @Singleton
fun providesIdentityApiService(retrofit: Retrofit): IdentityApi = retrofit.create() fun providesConfigApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigApi =
retrofit.create()
@Provides @Provides
@Singleton @Singleton
fun provideOkHttpClient(): OkHttpClient { fun providesIdentityApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): IdentityApi =
return OkHttpClient.Builder() retrofit.create()
.addInterceptor(
@Provides
@Singleton
fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor()
@Provides
@Singleton
fun providesOkHttpClientBuilder(): OkHttpClient.Builder =
OkHttpClient.Builder().addInterceptor(
HttpLoggingInterceptor().apply { HttpLoggingInterceptor().apply {
setLevel(HttpLoggingInterceptor.Level.BODY) setLevel(HttpLoggingInterceptor.Level.BODY)
}, },
) )
.build()
@Provides
@Singleton
fun providesRetrofitBuilder(
json: Json,
): Retrofit.Builder {
val contentType = "application/json".toMediaType()
return Retrofit.Builder().baseUrl("https://api.bitwarden.com")
.addConverterFactory(json.asConverterFactory(contentType))
.addCallAdapterFactory(ResultCallAdapterFactory())
} }
@Provides @Provides
@Singleton @Singleton
fun provideRetrofit( @Named(UNAUTHORIZED)
okHttpClient: OkHttpClient, fun providesUnauthorizedRetrofit(
json: Json, okHttpClientBuilder: OkHttpClient.Builder,
): Retrofit { retrofitBuilder: Retrofit.Builder,
val contentType = "application/json".toMediaType() ): Retrofit =
retrofitBuilder
return Retrofit.Builder() .client(
.baseUrl("https://api.bitwarden.com") okHttpClientBuilder.build(),
.client(okHttpClient) )
.addConverterFactory(json.asConverterFactory(contentType)) .build()
.addCallAdapterFactory(ResultCallAdapterFactory())
@Provides
@Singleton
@Named(AUTHORIZED)
fun providesAuthorizedRetrofit(
okHttpClientBuilder: OkHttpClient.Builder,
retrofitBuilder: Retrofit.Builder,
authTokenInterceptor: AuthTokenInterceptor,
): Retrofit =
retrofitBuilder
.client(
okHttpClientBuilder.addInterceptor(authTokenInterceptor).build(),
)
.build() .build()
}
@Provides @Provides
@Singleton @Singleton

View file

@ -0,0 +1,29 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
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 : Interceptor {
/**
* The auth token to be added to API requests.
*/
var authToken: String? = null
private val missingTokenMessage = "Auth token is missing!"
override fun intercept(chain: Interceptor.Chain): Response {
val token = authToken ?: throw IOException(IllegalStateException(missingTokenMessage))
val request = chain
.request()
.newBuilder()
.addHeader("Authorization", "Bearer $token")
.build()
return chain
.proceed(request)
}
}

View file

@ -0,0 +1,43 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import junit.framework.TestCase.assertEquals
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 interceptor: AuthTokenInterceptor = AuthTokenInterceptor()
private val mockAuthToken = "yourAuthToken"
private val request: Request = Request
.Builder()
.url("http://localhost")
.build()
@Test
fun `intercept should add the auth token when set`() {
interceptor.authToken = mockAuthToken
val response = interceptor.intercept(
chain = FakeInterceptorChain(request = request),
)
assertEquals(
"Bearer $mockAuthToken",
response.request.header("Authorization"),
)
}
@Test
fun `intercept should throw an exception when an auth token is missing`() {
val throwable = assertThrows(IOException::class.java) {
interceptor.intercept(
chain = FakeInterceptorChain(request = request),
)
}
assertEquals(
"Auth token is missing!",
throwable.cause?.message,
)
}
}

View file

@ -0,0 +1,69 @@
package com.x8bit.bitwarden.data.platform.datasource.network.interceptor
import okhttp3.Call
import okhttp3.Connection
import okhttp3.Interceptor
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import java.util.concurrent.TimeUnit
/**
* Helper class for implementing a [Interceptor.Chain] in a way that a [Request] passed in to
* [proceed] will be returned in a valid [Response] object that can be queried. This wrapping is
* performed by the [responseProvider].
*/
class FakeInterceptorChain(
private val request: Request,
private val responseProvider: (Request) -> Response = DEFAULT_RESPONSE_PROVIDER,
) : Interceptor.Chain {
override fun request(): Request = request
override fun proceed(request: Request): Response = responseProvider(request)
override fun connection(): Connection = notImplemented()
override fun call(): Call = notImplemented()
override fun connectTimeoutMillis(): Int = notImplemented()
override fun withConnectTimeout(
timeout: Int,
unit: TimeUnit,
): Interceptor.Chain = notImplemented()
override fun readTimeoutMillis(): Int = notImplemented()
override fun withReadTimeout(
timeout: Int,
unit: TimeUnit,
): Interceptor.Chain = notImplemented()
override fun writeTimeoutMillis(): Int = notImplemented()
override fun withWriteTimeout(
timeout: Int,
unit: TimeUnit,
): Interceptor.Chain = notImplemented()
private fun notImplemented(): Nothing {
throw NotImplementedError("This is not yet required by tests")
}
companion object {
/**
* A default response provider that provides a basic successful response. This is useful
* when the details of the response are not as important as retrieving the [Request] that
* was used to build it.
*/
val DEFAULT_RESPONSE_PROVIDER: (Request) -> Response = { request ->
Response
.Builder()
.code(200)
.message("OK")
.protocol(Protocol.HTTP_1_1)
.request(request)
.build()
}
}
}