From 3995aa75058586d181b6286187f7deaf5f7329dc Mon Sep 17 00:00:00 2001 From: Shannon Draeker <125921730+shannon-livefront@users.noreply.github.com> Date: Wed, 24 Jan 2024 15:54:25 -0700 Subject: [PATCH] Add support for different login methods (#762) --- .../datasource/network/api/IdentityApi.kt | 8 ++- .../network/model/IdentityTokenAuthModel.kt | 62 +++++++++++++++++++ .../network/model/TwoFactorDataModel.kt | 15 +++++ .../network/service/IdentityService.kt | 10 ++- .../network/service/IdentityServiceImpl.kt | 15 ++++- .../auth/repository/AuthRepositoryImpl.kt | 6 +- .../network/service/IdentityServiceTest.kt | 21 +++++-- .../auth/repository/AuthRepositoryTest.kt | 51 ++++++++++++--- 8 files changed, 167 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/IdentityTokenAuthModel.kt create mode 100644 app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TwoFactorDataModel.kt diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt index 8767237418..eb47b784f6 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/api/IdentityApi.kt @@ -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 @GET("/account/prevalidate") diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/IdentityTokenAuthModel.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/IdentityTokenAuthModel.kt new file mode 100644 index 0000000000..d978e6c5bf --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/IdentityTokenAuthModel.kt @@ -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 + } +} diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TwoFactorDataModel.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TwoFactorDataModel.kt new file mode 100644 index 0000000000..3ad69ed9fa --- /dev/null +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/model/TwoFactorDataModel.kt @@ -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, +) diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt index c49c73c1e6..9b945eb52d 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityService.kt @@ -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 /** diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt index 70253a1fbc..77d77bc1d7 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceImpl.kt @@ -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 = 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 -> diff --git a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt index 4b935ed342..38272a8ad1 100644 --- a/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt +++ b/app/src/main/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryImpl.kt @@ -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, ) } diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt index 8609221299..be56902ad7 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/datasource/network/service/IdentityServiceTest.kt @@ -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, ) diff --git a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt index db26506b63..72fdc60dfb 100644 --- a/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt +++ b/app/src/test/java/com/x8bit/bitwarden/data/auth/repository/AuthRepositoryTest.kt @@ -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, )