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 = "client_id") clientId: String,
@Field(value = "username") email: String, @Field(value = "username") email: String,
@Header(value = "auth-email") authEmail: String, @Header(value = "auth-email") authEmail: String,
@Field(value = "password") passwordHash: String, @Field(value = "password") passwordHash: String?,
@Field(value = "deviceIdentifier") deviceIdentifier: String, @Field(value = "deviceIdentifier") deviceIdentifier: String,
@Field(value = "deviceName") deviceName: String, @Field(value = "deviceName") deviceName: String,
@Field(value = "deviceType") deviceType: String, @Field(value = "deviceType") deviceType: String,
@Field(value = "grant_type") grantType: String, @Field(value = "grant_type") grantType: String,
@Field(value = "captchaResponse") captchaResponse: 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> ): Result<GetTokenResponseJson.Success>
@GET("/account/prevalidate") @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 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.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.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson 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. * Provides an API for querying identity endpoints.
@ -14,14 +16,18 @@ interface IdentityService {
* *
* @param uniqueAppId applications unique identifier. * @param uniqueAppId applications unique identifier.
* @param email user's email address. * @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 captchaToken captcha token to be passed to the API (nullable).
* @param twoFactorData the two-factor data, if applicable.
*/ */
@Suppress("LongParameterList")
suspend fun getToken( suspend fun getToken(
uniqueAppId: String, uniqueAppId: String,
email: String, email: String,
passwordHash: String, authModel: IdentityTokenAuthModel,
captchaToken: String?, captchaToken: String?,
twoFactorData: TwoFactorDataModel? = null,
): Result<GetTokenResponseJson> ): 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.api.IdentityApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson 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.PrevalidateSsoResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson 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.model.toBitwardenError
import com.x8bit.bitwarden.data.platform.datasource.network.util.base64UrlEncode 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.executeForResult
@ -21,8 +23,9 @@ class IdentityServiceImpl constructor(
override suspend fun getToken( override suspend fun getToken(
uniqueAppId: String, uniqueAppId: String,
email: String, email: String,
passwordHash: String, authModel: IdentityTokenAuthModel,
captchaToken: String?, captchaToken: String?,
twoFactorData: TwoFactorDataModel?,
): Result<GetTokenResponseJson> = api ): Result<GetTokenResponseJson> = api
.getToken( .getToken(
scope = "api+offline_access", scope = "api+offline_access",
@ -31,9 +34,15 @@ class IdentityServiceImpl constructor(
deviceIdentifier = uniqueAppId, deviceIdentifier = uniqueAppId,
deviceName = deviceModelProvider.deviceModel, deviceName = deviceModelProvider.deviceModel,
deviceType = "0", deviceType = "0",
grantType = "password", grantType = authModel.grantType,
passwordHash = passwordHash, passwordHash = authModel.password,
email = email, 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, captchaResponse = captchaToken,
) )
.recoverCatching { throwable -> .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
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson.CaptchaRequired 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.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.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.RefreshTokenResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson import com.x8bit.bitwarden.data.auth.datasource.network.model.RegisterRequestJson
@ -194,7 +195,10 @@ class AuthRepositoryImpl(
identityService.getToken( identityService.getToken(
uniqueAppId = authDiskSource.uniqueAppId, uniqueAppId = authDiskSource.uniqueAppId,
email = email, email = email,
passwordHash = passwordHash, authModel = IdentityTokenAuthModel.MasterPassword(
username = email,
password = passwordHash,
),
captchaToken = captchaToken, 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.api.IdentityApi
import com.x8bit.bitwarden.data.auth.datasource.network.model.GetTokenResponseJson 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.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.KeyConnectorUserDecryptionOptionsJson 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.MasterPasswordPolicyOptionsJson
@ -40,7 +41,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON)) server.enqueue(MockResponse().setBody(LOGIN_SUCCESS_JSON))
val result = identityService.getToken( val result = identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -52,7 +56,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(500)) server.enqueue(MockResponse().setResponseCode(500))
val result = identityService.getToken( val result = identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -64,7 +71,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON)) server.enqueue(MockResponse().setResponseCode(400).setBody(CAPTCHA_BODY_JSON))
val result = identityService.getToken( val result = identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -76,7 +86,10 @@ class IdentityServiceTest : BaseServiceTest() {
server.enqueue(MockResponse().setResponseCode(400).setBody(INVALID_LOGIN_JSON)) server.enqueue(MockResponse().setResponseCode(400).setBody(INVALID_LOGIN_JSON))
val result = identityService.getToken( val result = identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, 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.disk.util.FakeAuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.network.model.AuthRequestsResponseJson 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.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.KdfTypeJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.PasswordHintResponseJson
import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson import com.x8bit.bitwarden.data.auth.datasource.network.model.PreLoginResponseJson
@ -444,7 +445,10 @@ class AuthRepositoryTest {
coEvery { coEvery {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -457,7 +461,10 @@ class AuthRepositoryTest {
coVerify { coVerify {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -472,7 +479,10 @@ class AuthRepositoryTest {
coEvery { coEvery {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -491,7 +501,10 @@ class AuthRepositoryTest {
coVerify { coVerify {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -509,7 +522,10 @@ class AuthRepositoryTest {
coEvery { coEvery {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -548,7 +564,10 @@ class AuthRepositoryTest {
coVerify { coVerify {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -587,7 +606,10 @@ class AuthRepositoryTest {
coEvery { coEvery {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -628,7 +650,10 @@ class AuthRepositoryTest {
coVerify { coVerify {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -658,7 +683,10 @@ class AuthRepositoryTest {
coEvery { coEvery {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )
@ -671,7 +699,10 @@ class AuthRepositoryTest {
coVerify { coVerify {
identityService.getToken( identityService.getToken(
email = EMAIL, email = EMAIL,
passwordHash = PASSWORD_HASH, authModel = IdentityTokenAuthModel.MasterPassword(
username = EMAIL,
password = PASSWORD_HASH,
),
captchaToken = null, captchaToken = null,
uniqueAppId = UNIQUE_APP_ID, uniqueAppId = UNIQUE_APP_ID,
) )