mirror of
https://github.com/bitwarden/android.git
synced 2024-11-23 18:06:08 +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.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
|
||||||
},
|
},
|
||||||
|
|
|
@ -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,46 +27,75 @@ 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(
|
|
||||||
HttpLoggingInterceptor().apply {
|
|
||||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun provideRetrofit(
|
fun providesAuthTokenInterceptor(): AuthTokenInterceptor = AuthTokenInterceptor()
|
||||||
okHttpClient: OkHttpClient,
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesOkHttpClientBuilder(): OkHttpClient.Builder =
|
||||||
|
OkHttpClient.Builder().addInterceptor(
|
||||||
|
HttpLoggingInterceptor().apply {
|
||||||
|
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesRetrofitBuilder(
|
||||||
json: Json,
|
json: Json,
|
||||||
): Retrofit {
|
): Retrofit.Builder {
|
||||||
val contentType = "application/json".toMediaType()
|
val contentType = "application/json".toMediaType()
|
||||||
|
return Retrofit.Builder().baseUrl("https://api.bitwarden.com")
|
||||||
return Retrofit.Builder()
|
|
||||||
.baseUrl("https://api.bitwarden.com")
|
|
||||||
.client(okHttpClient)
|
|
||||||
.addConverterFactory(json.asConverterFactory(contentType))
|
.addConverterFactory(json.asConverterFactory(contentType))
|
||||||
.addCallAdapterFactory(ResultCallAdapterFactory())
|
.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
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
fun providesJson(): Json = Json {
|
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