mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 09:56:11 +03:00
BIT-405: AuthTokenInterceptor (#57)
This commit is contained in:
parent
016f597d8c
commit
046a94939e
5 changed files with 198 additions and 22 deletions
|
@ -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.LoginResult
|
||||
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 kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
@ -22,6 +23,7 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
private val accountsApi: AccountsApi,
|
||||
private val identityApi: IdentityApi,
|
||||
private val bitwardenSdkClient: Client,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
) : AuthRepository {
|
||||
|
||||
private val mutableAuthStateFlow = MutableStateFlow<AuthState>(AuthState.Unauthenticated)
|
||||
|
@ -55,6 +57,8 @@ class AuthRepositoryImpl @Inject constructor(
|
|||
LoginResult.Error
|
||||
},
|
||||
onSuccess = {
|
||||
// TODO: Create intermediate class for providing auth token to interceptor (BIT-411)
|
||||
authTokenInterceptor.authToken = it.accessToken
|
||||
mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken)
|
||||
LoginResult.Success
|
||||
},
|
||||
|
|
|
@ -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.platform.datasource.network.api.ConfigApi
|
||||
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.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
|
@ -15,6 +16,7 @@ import okhttp3.OkHttpClient
|
|||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
|
@ -25,46 +27,75 @@ import javax.inject.Singleton
|
|||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountsApiService(retrofit: Retrofit): AccountsApi = retrofit.create()
|
||||
private const val AUTHORIZED = "authorized"
|
||||
private const val UNAUTHORIZED = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigApiService(retrofit: Retrofit): ConfigApi = retrofit.create()
|
||||
fun providesAccountsApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): AccountsApi =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityApiService(retrofit: Retrofit): IdentityApi = retrofit.create()
|
||||
fun providesConfigApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigApi =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
}
|
||||
fun providesIdentityApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): IdentityApi =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRetrofit(
|
||||
okHttpClient: OkHttpClient,
|
||||
fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesOkHttpClientBuilder(): OkHttpClient.Builder =
|
||||
OkHttpClient.Builder().addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
},
|
||||
)
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesRetrofitBuilder(
|
||||
json: Json,
|
||||
): Retrofit {
|
||||
): Retrofit.Builder {
|
||||
val contentType = "application/json".toMediaType()
|
||||
|
||||
return Retrofit.Builder()
|
||||
.baseUrl("https://api.bitwarden.com")
|
||||
.client(okHttpClient)
|
||||
return Retrofit.Builder().baseUrl("https://api.bitwarden.com")
|
||||
.addConverterFactory(json.asConverterFactory(contentType))
|
||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(UNAUTHORIZED)
|
||||
fun providesUnauthorizedRetrofit(
|
||||
okHttpClientBuilder: OkHttpClient.Builder,
|
||||
retrofitBuilder: Retrofit.Builder,
|
||||
): Retrofit =
|
||||
retrofitBuilder
|
||||
.client(
|
||||
okHttpClientBuilder.build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(AUTHORIZED)
|
||||
fun providesAuthorizedRetrofit(
|
||||
okHttpClientBuilder: OkHttpClient.Builder,
|
||||
retrofitBuilder: Retrofit.Builder,
|
||||
authTokenInterceptor: AuthTokenInterceptor,
|
||||
): Retrofit =
|
||||
retrofitBuilder
|
||||
.client(
|
||||
okHttpClientBuilder.addInterceptor(authTokenInterceptor).build(),
|
||||
)
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesJson(): Json = Json {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue