mirror of
https://github.com/bitwarden/android.git
synced 2026-06-08 23:16:33 -05:00
BIT-394 Setup service layer to accommodate get token error parsing (#61)
This commit is contained in:
committed by
Álison Fernandes
parent
9d7990026c
commit
e69049d597
@@ -6,7 +6,7 @@ import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
/**
|
||||
* Defines calls under the /accounts API.
|
||||
* Defines raw calls under the /accounts API.
|
||||
*/
|
||||
interface AccountsApi {
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Header
|
||||
import retrofit2.http.POST
|
||||
import retrofit2.http.Url
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Defines calls under the /identity API.
|
||||
* Defines raw calls under the /identity API.
|
||||
*/
|
||||
interface IdentityApi {
|
||||
|
||||
@@ -18,18 +16,15 @@ interface IdentityApi {
|
||||
@Suppress("LongParameterList")
|
||||
@FormUrlEncoded
|
||||
suspend fun getToken(
|
||||
// TODO: use correct base URL here BIT-328
|
||||
@Url url: String = "https://vault.bitwarden.com/identity/connect/token",
|
||||
@Field(value = "scope", encoded = true) scope: String = "api+offline_access",
|
||||
@Field(value = "client_id") clientId: String = "mobile",
|
||||
@Url url: String,
|
||||
@Field(value = "scope", encoded = true) scope: String,
|
||||
@Field(value = "client_id") clientId: String,
|
||||
@Field(value = "username") email: String,
|
||||
@Header(value = "auth-email") authEmail: String = email.base64UrlEncode(),
|
||||
@Header(value = "auth-email") authEmail: String,
|
||||
@Field(value = "password") passwordHash: String,
|
||||
// TODO: use correct device identifier here BIT-325
|
||||
@Field(value = "deviceIdentifier") deviceIdentifier: String = UUID.randomUUID().toString(),
|
||||
// TODO: use correct values for deviceName and deviceType BIT-326
|
||||
@Field(value = "deviceName") deviceName: String = "Pixel 6",
|
||||
@Field(value = "deviceType") deviceType: String = "1",
|
||||
@Field(value = "grant_type") grantType: String = "password",
|
||||
): Result<GetTokenResponseJson>
|
||||
@Field(value = "deviceIdentifier") deviceIdentifier: String,
|
||||
@Field(value = "deviceName") deviceName: String,
|
||||
@Field(value = "deviceType") deviceType: String,
|
||||
@Field(value = "grant_type") grantType: String,
|
||||
): Result<GetTokenResponseJson.Success>
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.di
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.AccountsServiceImpl
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityServiceImpl
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.di.NetworkModule
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.create
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Provides network dependencies in the auth package.
|
||||
*/
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
): AccountsService = AccountsServiceImpl(retrofit.create())
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityService(
|
||||
@Named(NetworkModule.UNAUTHORIZED) retrofit: Retrofit,
|
||||
json: Json,
|
||||
): IdentityService = IdentityServiceImpl(retrofit.create(), json)
|
||||
}
|
||||
@@ -4,12 +4,26 @@ import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models json response of the get token request.
|
||||
*
|
||||
* @param accessToken the access token.
|
||||
* Models response bodies from the get token request.
|
||||
*/
|
||||
@Serializable
|
||||
data class GetTokenResponseJson(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
)
|
||||
sealed class GetTokenResponseJson {
|
||||
/**
|
||||
* Models json response of the get token request.
|
||||
*
|
||||
* @param accessToken the access token.
|
||||
*/
|
||||
@Serializable
|
||||
data class Success(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
) : GetTokenResponseJson()
|
||||
|
||||
/**
|
||||
* Models json body of a captcha error.
|
||||
*/
|
||||
@Serializable
|
||||
data class CaptchaRequired(
|
||||
@SerialName("HCaptcha_SiteKey")
|
||||
val captchaKey: String,
|
||||
) : GetTokenResponseJson()
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ sealed class LoginResult {
|
||||
*/
|
||||
data object Success : LoginResult()
|
||||
|
||||
/**
|
||||
* Captcha verification is required.
|
||||
*/
|
||||
data class CaptchaRequired(val captchaId: String) : LoginResult()
|
||||
|
||||
/**
|
||||
* There was an error logging in.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
|
||||
/**
|
||||
* Wraps raw retrofit accounts API in a cleaner interface.
|
||||
*/
|
||||
interface AccountsService {
|
||||
|
||||
/**
|
||||
* Make pre login request to get KDF params.
|
||||
*/
|
||||
suspend fun preLogin(email: String): Result<PreLoginResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.AccountsApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginRequestJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
|
||||
|
||||
class AccountsServiceImpl constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
) : AccountsService {
|
||||
|
||||
override suspend fun preLogin(email: String): Result<PreLoginResponseJson> =
|
||||
accountsApi.preLogin(PreLoginRequestJson(email = email))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
|
||||
/**
|
||||
* Wraps raw retrofit identity API in a cleaner interface.
|
||||
*/
|
||||
interface IdentityService {
|
||||
|
||||
/**
|
||||
* Make request to get an access token.
|
||||
*
|
||||
* @param email user's email address.
|
||||
* @param passwordHash password hashed with the Bitwarden SDK.
|
||||
*/
|
||||
suspend fun getToken(
|
||||
email: String,
|
||||
passwordHash: String,
|
||||
): Result<GetTokenResponseJson>
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.api.IdentityApi
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyAsResult
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||
import java.util.UUID
|
||||
|
||||
class IdentityServiceImpl constructor(
|
||||
private val api: IdentityApi,
|
||||
private val json: Json,
|
||||
// TODO: use correct base URL here BIT-328
|
||||
private val baseUrl: String = "https://vault.bitwarden.com",
|
||||
) : IdentityService {
|
||||
|
||||
override suspend fun getToken(
|
||||
email: String,
|
||||
passwordHash: String,
|
||||
): Result<GetTokenResponseJson> = api
|
||||
.getToken(
|
||||
// TODO: use correct base URL here BIT-328
|
||||
url = "$baseUrl/identity/connect/token",
|
||||
scope = "api+offline_access",
|
||||
clientId = "mobile",
|
||||
authEmail = email.base64UrlEncode(),
|
||||
// TODO: use correct device identifier here BIT-325
|
||||
deviceIdentifier = UUID.randomUUID().toString(),
|
||||
// TODO: use correct values for deviceName and deviceType BIT-326
|
||||
deviceName = "Pixel 6",
|
||||
deviceType = "1",
|
||||
grantType = "password",
|
||||
passwordHash = passwordHash,
|
||||
email = email,
|
||||
)
|
||||
.fold(
|
||||
onSuccess = { Result.success(it) },
|
||||
onFailure = {
|
||||
it.parseErrorBodyAsResult<GetTokenResponseJson.CaptchaRequired>(
|
||||
code = HTTP_BAD_REQUEST,
|
||||
json = json,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2,11 +2,12 @@ package com.x8bit.bitwarden.data.auth.repository
|
||||
|
||||
import com.bitwarden.core.Kdf
|
||||
import com.bitwarden.sdk.Client
|
||||
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.model.AuthState
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.Success
|
||||
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.service.AccountsService
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.service.IdentityService
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.interceptor.AuthTokenInterceptor
|
||||
import com.x8bit.bitwarden.data.platform.util.flatMap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -20,8 +21,8 @@ import javax.inject.Singleton
|
||||
*/
|
||||
@Singleton
|
||||
class AuthRepositoryImpl @Inject constructor(
|
||||
private val accountsApi: AccountsApi,
|
||||
private val identityApi: IdentityApi,
|
||||
private val accountsService: AccountsService,
|
||||
private val identityService: IdentityService,
|
||||
private val bitwardenSdkClient: Client,
|
||||
private val authTokenInterceptor: AuthTokenInterceptor,
|
||||
) : AuthRepository {
|
||||
@@ -35,8 +36,8 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
override suspend fun login(
|
||||
email: String,
|
||||
password: String,
|
||||
): LoginResult = accountsApi
|
||||
.preLogin(PreLoginRequestJson(email))
|
||||
): LoginResult = accountsService
|
||||
.preLogin(email = email)
|
||||
.flatMap {
|
||||
// TODO: Use KDF enum from pre login correctly (BIT-329)
|
||||
val passwordHash = bitwardenSdkClient
|
||||
@@ -46,21 +47,27 @@ class AuthRepositoryImpl @Inject constructor(
|
||||
password = password,
|
||||
kdfParams = Kdf.Pbkdf2(it.kdfIterations),
|
||||
)
|
||||
identityApi.getToken(
|
||||
identityService.getToken(
|
||||
email = email,
|
||||
passwordHash = passwordHash,
|
||||
)
|
||||
}
|
||||
.fold(
|
||||
onFailure = {
|
||||
// TODO: Add more detail to these cases to expose server error messages (BIT-320)
|
||||
// TODO: Add more detail to error case to expose server error messages (BIT-320)
|
||||
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
|
||||
when (it) {
|
||||
is CaptchaRequired -> LoginResult.CaptchaRequired(it.captchaKey)
|
||||
is Success -> {
|
||||
// TODO: Create intermediate class for providing auth token
|
||||
// to interceptor (BIT-411)
|
||||
authTokenInterceptor.authToken = it.accessToken
|
||||
mutableAuthStateFlow.value = AuthState.Authenticated(it.accessToken)
|
||||
LoginResult.Success
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.di
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
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
|
||||
@@ -27,23 +25,23 @@ import javax.inject.Singleton
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
private const val AUTHORIZED = "authorized"
|
||||
private const val UNAUTHORIZED = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesAccountsApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): AccountsApi =
|
||||
retrofit.create()
|
||||
const val AUTHORIZED: String = "authorized"
|
||||
const val UNAUTHORIZED: String = "unauthorized"
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesConfigApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): ConfigApi =
|
||||
retrofit.create()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun providesIdentityApiService(@Named(UNAUTHORIZED) retrofit: Retrofit): IdentityApi =
|
||||
retrofit.create()
|
||||
fun provideOkHttpClient(): OkHttpClient {
|
||||
return OkHttpClient.Builder()
|
||||
.addInterceptor(
|
||||
HttpLoggingInterceptor().apply {
|
||||
setLevel(HttpLoggingInterceptor.Level.BODY)
|
||||
},
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import retrofit2.HttpException
|
||||
|
||||
/**
|
||||
* Attempt to parse the error body to serializable type [T].
|
||||
*
|
||||
* Useful in service layer for parsing non-200 response bodies.
|
||||
*
|
||||
* If the receiver is not an [HttpException] or the error body cannot be parsed, the original
|
||||
* Throwable will be returned as a Result.failure.
|
||||
*
|
||||
* @param code HTTP code associated with the error. Only responses with this code will be attempted
|
||||
* to be parsed.
|
||||
* @param json [Json] serializer to use.
|
||||
*/
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
inline fun <reified T> Throwable.parseErrorBodyAsResult(code: Int, json: Json): Result<T> =
|
||||
(this as? HttpException)
|
||||
?.response()
|
||||
?.takeIf { it.code() == code }
|
||||
?.errorBody()
|
||||
?.let { errorBody ->
|
||||
try {
|
||||
Result.success(json.decodeFromStream(errorBody.byteStream()))
|
||||
} catch (_: Exception) {
|
||||
Result.failure(this)
|
||||
}
|
||||
} ?: Result.failure(this)
|
||||
@@ -60,6 +60,8 @@ class LoginViewModel @Inject constructor(
|
||||
LoginResult.Error -> Unit
|
||||
// No action required on success, root nav will navigate to logged in state
|
||||
LoginResult.Success -> Unit
|
||||
// TODO: launch intent with captcha URL BIT-399
|
||||
is LoginResult.CaptchaRequired -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user