Add support for different login methods (#762)

This commit is contained in:
Shannon Draeker 2024-01-24 15:54:25 -07:00 committed by Álison Fernandes
parent c977f7617a
commit 410e3072fa
8 changed files with 167 additions and 21 deletions

View file

@ -24,12 +24,18 @@ interface IdentityApi {
@Field(value = "client_id") clientId: String,
@Field(value = "username") email: String,
@Header(value = "auth-email") authEmail: String,
@Field(value = "password") passwordHash: String,
@Field(value = "password") passwordHash: String?,
@Field(value = "deviceIdentifier") deviceIdentifier: String,
@Field(value = "deviceName") deviceName: String,
@Field(value = "deviceType") deviceType: String,
@Field(value = "grant_type") grantType: String,
@Field(value = "captchaResponse") captchaResponse: String?,
@Field(value = "code") ssoCode: String?,
@Field(value = "code_verifier") ssoCodeVerifier: String?,
@Field(value = "redirect_uri") ssoRedirectUri: String?,
@Field(value = "twoFactorToken") twoFactorCode: String?,
@Field(value = "twoFactorProvider") twoFactorMethod: String?,
@Field(value = "twoFactorRemember") twoFactorRemember: String?,
): Result<GetTokenResponseJson.Success>
@GET("/account/prevalidate")

View file

@ -0,0 +1,62 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
/**
* Hold the authentication information for different login methods.
*/
sealed class IdentityTokenAuthModel {
/**
* The type of authentication.
*/
abstract val grantType: String
/**
* The username for login with password.
*/
abstract val username: String?
/**
* The password for login with password.
*/
abstract val password: String?
/**
* The sso code for login with single sign on.
*/
abstract val ssoCode: String?
/**
* The sso code verifier for login with single sign on.
*/
abstract val ssoCodeVerifier: String?
/**
* The sso redirect uri for login with single sign on.
*/
abstract val ssoRedirectUri: String?
/**
* The data for logging in with a username and password.
*/
data class MasterPassword(
override val username: String,
override val password: String,
) : IdentityTokenAuthModel() {
override val grantType: String get() = "password"
override val ssoCode: String? get() = null
override val ssoCodeVerifier: String? get() = null
override val ssoRedirectUri: String? get() = null
}
/**
* The data for logging in with single sign on credentials.
*/
data class SingleSignOn(
override val ssoCode: String,
override val ssoCodeVerifier: String,
override val ssoRedirectUri: String,
) : IdentityTokenAuthModel() {
override val grantType: String get() = "authorization_code"
override val username: String? get() = null
override val password: String? get() = null
}
}

View file

@ -0,0 +1,15 @@
package com.x8bit.bitwarden.data.auth.datasource.network.model
/**
* Hold the information necessary to add two-factor authorization
* to a login request.
*
* @property code The two-factor code.
* @property method The two-factor method.
* @property remember The two-factor remember setting.
*/
data class TwoFactorDataModel(
val code: String,
val method: String,
val remember: Boolean,
)

View file

@ -1,8 +1,10 @@
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.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
/**
* Provides an API for querying identity endpoints.
@ -14,14 +16,18 @@ interface IdentityService {
*
* @param uniqueAppId applications unique identifier.
* @param email user's email address.
* @param passwordHash password hashed with the Bitwarden SDK.
* @param authModel information necessary to authenticate with any
* of the available login methods.
* @param captchaToken captcha token to be passed to the API (nullable).
* @param twoFactorData the two-factor data, if applicable.
*/
@Suppress("LongParameterList")
suspend fun getToken(
uniqueAppId: String,
email: String,
passwordHash: String,
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel? = null,
): Result<GetTokenResponseJson>
/**

View file

@ -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.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.TwoFactorDataModel
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
@ -21,8 +23,9 @@ class IdentityServiceImpl constructor(
override suspend fun getToken(
uniqueAppId: String,
email: String,
passwordHash: String,
authModel: IdentityTokenAuthModel,
captchaToken: String?,
twoFactorData: TwoFactorDataModel?,
): Result<GetTokenResponseJson> = api
.getToken(
scope = "api+offline_access",
@ -31,9 +34,15 @@ class IdentityServiceImpl constructor(
deviceIdentifier = uniqueAppId,
deviceName = deviceModelProvider.deviceModel,
deviceType = "0",
grantType = "password",
passwordHash = passwordHash,
grantType = authModel.grantType,
passwordHash = authModel.password,
email = email,
ssoCode = authModel.ssoCode,
ssoCodeVerifier = authModel.ssoCodeVerifier,
ssoRedirectUri = authModel.ssoRedirectUri,
twoFactorCode = twoFactorData?.code,
twoFactorMethod = twoFactorData?.method,
twoFactorRemember = twoFactorData?.remember?.let { if (it) "1" else "0 " },
captchaResponse = captchaToken,
)
.recoverCatching { throwable ->

View file

@ -7,6 +7,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
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.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
@ -194,7 +195,10 @@ class AuthRepositoryImpl(
identityService.getToken(
uniqueAppId = authDiskSource.uniqueAppId,
email = email,
passwordHash = passwordHash,
authModel = IdentityTokenAuthModel.MasterPassword(
username = email,
password = passwordHash,
),
captchaToken = captchaToken,
)
}

View file

@ -2,6 +2,7 @@ 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.IdentityTokenAuthModel
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
@ -40,7 +41,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
val result = identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -52,7 +56,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(500))
val result = identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -64,7 +71,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON))
val result = identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -76,7 +86,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(400).setBody(INVALID_LOGIN_JSON))
val result = identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)

View file

@ -11,6 +11,7 @@ import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson
import com.x8bit.bitwarden.data.auth.datasource.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.IdentityTokenAuthModel
import com.x8bit.bitwarden.data.auth.datasource.network.model.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
@ -444,7 +445,10 @@ class AuthRepositoryTest {
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -457,7 +461,10 @@ class AuthRepositoryTest {
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -472,7 +479,10 @@ class AuthRepositoryTest {
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -491,7 +501,10 @@ class AuthRepositoryTest {
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -509,7 +522,10 @@ class AuthRepositoryTest {
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -548,7 +564,10 @@ class AuthRepositoryTest {
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -587,7 +606,10 @@ class AuthRepositoryTest {
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -628,7 +650,10 @@ class AuthRepositoryTest {
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -658,7 +683,10 @@ class AuthRepositoryTest {
coEvery {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)
@ -671,7 +699,10 @@ class AuthRepositoryTest {
coVerify {
identityService.getToken(
email = EMAIL,
passwordHash = PASSWORD_HASH,
authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null,
uniqueAppId = UNIQUE_APP_ID,
)