mirror of
https://github.com/bitwarden/android.git
synced 2024-11-27 03:49:36 +03:00
Add synchronous refresh token API (#271)
This commit is contained in:
parent
8c0c606d72
commit
3a07bbd3da
8 changed files with 200 additions and 3 deletions
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.api
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import retrofit2.Call
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.Header
|
||||
|
@ -26,4 +28,16 @@ interface IdentityApi {
|
|||
@Field(value = "grant_type") grantType: String,
|
||||
@Field(value = "captchaResponse") captchaResponse: String?,
|
||||
): Result<GetTokenResponseJson.Success>
|
||||
|
||||
/**
|
||||
* This call needs to be synchronous so we need it to return a [Call] directly. The identity
|
||||
* service will wrap it up for us.
|
||||
*/
|
||||
@POST("/connect/token")
|
||||
@FormUrlEncoded
|
||||
fun refreshTokenCall(
|
||||
@Field(value = "client_id") clientId: String,
|
||||
@Field(value = "refresh_token") refreshToken: String,
|
||||
@Field(value = "grant_type") grantType: String,
|
||||
): Call<RefreshTokenResponseJson>
|
||||
}
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.model
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Models the response body from the refresh token request.
|
||||
*
|
||||
* @property accessToken The new access token.
|
||||
* @property expiresIn When the new [accessToken] expires.
|
||||
* @property refreshToken The new refresh token.
|
||||
* @property tokenType The type of token the new [accessToken] is.
|
||||
*/
|
||||
@Serializable
|
||||
data class RefreshTokenResponseJson(
|
||||
@SerialName("access_token")
|
||||
val accessToken: String,
|
||||
|
||||
@SerialName("expires_in")
|
||||
val expiresIn: Int,
|
||||
|
||||
@SerialName("refresh_token")
|
||||
val refreshToken: String,
|
||||
|
||||
@SerialName("token_type")
|
||||
val tokenType: String,
|
||||
)
|
|
@ -1,6 +1,7 @@
|
|||
package com.x8bit.bitwarden.data.auth.datasource.network.service
|
||||
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
|
||||
/**
|
||||
* Provides an API for querying identity endpoints.
|
||||
|
@ -19,4 +20,11 @@ interface IdentityService {
|
|||
passwordHash: String,
|
||||
captchaToken: String?,
|
||||
): Result<GetTokenResponseJson>
|
||||
|
||||
/**
|
||||
* Synchronously makes a request to get refresh the access token.
|
||||
*
|
||||
* @param refreshToken The refresh token needed to obtain a new token.
|
||||
*/
|
||||
fun refreshTokenSynchronously(refreshToken: String): Result<RefreshTokenResponseJson>
|
||||
}
|
||||
|
|
|
@ -2,8 +2,10 @@ 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.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.model.toBitwardenError
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.executeForResult
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.util.parseErrorBodyOrNull
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import kotlinx.serialization.json.Json
|
||||
|
@ -44,4 +46,14 @@ class IdentityServiceImpl constructor(
|
|||
json = json,
|
||||
) ?: throw throwable
|
||||
}
|
||||
|
||||
override fun refreshTokenSynchronously(
|
||||
refreshToken: String,
|
||||
): Result<RefreshTokenResponseJson> = api
|
||||
.refreshTokenCall(
|
||||
clientId = "mobile",
|
||||
grantType = "refresh_token",
|
||||
refreshToken = refreshToken,
|
||||
)
|
||||
.executeForResult()
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.core
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.asFailure
|
||||
import okhttp3.Request
|
||||
import okio.IOException
|
||||
import okio.Timeout
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
|
@ -48,9 +50,29 @@ class ResultCall<T>(
|
|||
},
|
||||
)
|
||||
|
||||
override fun execute(): Response<Result<T>> = throw UnsupportedOperationException(
|
||||
"This call can't be executed synchronously",
|
||||
)
|
||||
/**
|
||||
* Synchronously send the request and return its response as a [Result].
|
||||
*/
|
||||
fun executeForResult(): Result<T> = requireNotNull(execute().body())
|
||||
|
||||
@Suppress("ReturnCount", "TooGenericExceptionCaught")
|
||||
override fun execute(): Response<Result<T>> {
|
||||
val response = try {
|
||||
backingCall.execute()
|
||||
} catch (ioException: IOException) {
|
||||
return success(ioException.asFailure())
|
||||
} catch (runtimeException: RuntimeException) {
|
||||
return success(runtimeException.asFailure())
|
||||
}
|
||||
|
||||
return success(
|
||||
if (!response.isSuccessful) {
|
||||
HttpException(response).asFailure()
|
||||
} else {
|
||||
createResult(response.body())
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
override fun isCanceled(): Boolean = backingCall.isCanceled
|
||||
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.datasource.network.core.ResultCall
|
||||
import retrofit2.Call
|
||||
|
||||
/**
|
||||
* Synchronously executes the [Call] and returns the [Result].
|
||||
*/
|
||||
inline fun <reified T : Any> Call<T>.executeForResult(): Result<T> =
|
||||
this
|
||||
.toResultCall()
|
||||
.executeForResult()
|
||||
|
||||
/**
|
||||
* Wraps the existing [Call] in a [ResultCall].
|
||||
*/
|
||||
inline fun <reified T : Any> Call<T>.toResultCall(): ResultCall<T> =
|
||||
ResultCall(
|
||||
backingCall = this,
|
||||
successType = T::class.java,
|
||||
)
|
|
@ -5,10 +5,12 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJs
|
|||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.MasterPasswordPolicyOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.TrustedDeviceUserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.UserDecryptionOptionsJson
|
||||
import com.x8bit.bitwarden.data.platform.base.BaseServiceTest
|
||||
import com.x8bit.bitwarden.data.platform.util.DeviceModelProvider
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
|
@ -76,12 +78,44 @@ class IdentityServiceTest : BaseServiceTest() {
|
|||
assertEquals(Result.success(INVALID_LOGIN), result)
|
||||
}
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
@Test
|
||||
fun `refreshTokenSynchronously when response is success should return RefreshTokenResponseJson`() {
|
||||
server.enqueue(MockResponse().setResponseCode(200).setBody(REFRESH_TOKEN_JSON))
|
||||
val result = identityService.refreshTokenSynchronously(refreshToken = REFRESH_TOKEN)
|
||||
assertEquals(REFRESH_TOKEN_BODY.asSuccess(), result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `refreshTokenSynchronously when response is an error should return an error`() {
|
||||
server.enqueue(MockResponse().setResponseCode(400))
|
||||
val result = identityService.refreshTokenSynchronously(refreshToken = REFRESH_TOKEN)
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val REFRESH_TOKEN = "refreshToken"
|
||||
private const val EMAIL = "email"
|
||||
private const val PASSWORD_HASH = "passwordHash"
|
||||
}
|
||||
}
|
||||
|
||||
private const val REFRESH_TOKEN_JSON = """
|
||||
{
|
||||
"access_token": "accessToken",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "refreshToken",
|
||||
"token_type": "Bearer"
|
||||
}
|
||||
"""
|
||||
|
||||
private val REFRESH_TOKEN_BODY = RefreshTokenResponseJson(
|
||||
accessToken = "accessToken",
|
||||
expiresIn = 3600,
|
||||
refreshToken = "refreshToken",
|
||||
tokenType = "Bearer",
|
||||
)
|
||||
|
||||
private const val CAPTCHA_BODY_JSON = """
|
||||
{
|
||||
"HCaptcha_SiteKey": "123"
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
package com.x8bit.bitwarden.data.platform.datasource.network.util
|
||||
|
||||
import com.x8bit.bitwarden.data.platform.util.asSuccess
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import retrofit2.Call
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
class CallExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when execute throws IOException`() {
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { execute() } throws IOException("Fail")
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when execute throws RuntimeException`() {
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { execute() } throws RuntimeException("Fail")
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns failure when response is failure`() {
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { execute() } returns Response.error(400, "".toResponseBody())
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `executeForResult returns success when response is failure`() {
|
||||
val call = mockk<Call<Unit>> {
|
||||
every { execute() } returns Response.success(Unit)
|
||||
}
|
||||
|
||||
val result = call.executeForResult()
|
||||
|
||||
assertEquals(Unit.asSuccess(), result)
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue