mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 10:25:57 +03:00
PM-11254: Add logic for logging in with Key Connector (#3802)
This commit is contained in:
parent
e7bd966e94
commit
a0a5070ac7
2 changed files with 531 additions and 11 deletions
|
@ -1455,13 +1455,20 @@ class AuthRepositoryImpl(
|
||||||
previousUserState = authDiskSource.userState,
|
previousUserState = authDiskSource.userState,
|
||||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||||
)
|
)
|
||||||
val userId = userStateJson.activeUserId
|
val profile = userStateJson.activeAccount.profile
|
||||||
|
val userId = profile.userId
|
||||||
|
|
||||||
checkForVaultUnlockError(
|
checkForVaultUnlockError(
|
||||||
onVaultUnlockError = { vaultUnlockError ->
|
onVaultUnlockError = { vaultUnlockError ->
|
||||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
val keyConnectorUrl = loginResponse
|
||||||
|
.keyConnectorUrl
|
||||||
|
?: loginResponse
|
||||||
|
.userDecryptionOptions
|
||||||
|
?.keyConnectorUserDecryptionOptions
|
||||||
|
?.keyConnectorUrl
|
||||||
val isDeviceUnlockAvailable = deviceData != null ||
|
val isDeviceUnlockAvailable = deviceData != null ||
|
||||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||||
// if possible attempt to unlock the vault with trusted device data
|
// if possible attempt to unlock the vault with trusted device data
|
||||||
|
@ -1471,14 +1478,19 @@ class AuthRepositoryImpl(
|
||||||
userStateJson = userStateJson,
|
userStateJson = userStateJson,
|
||||||
deviceData = deviceData,
|
deviceData = deviceData,
|
||||||
)
|
)
|
||||||
|
} else if (keyConnectorUrl != null && orgIdentifier != null) {
|
||||||
|
unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||||
|
profile = profile,
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
orgIdentifier = orgIdentifier,
|
||||||
|
loginResponse = loginResponse,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
password?.let {
|
unlockVaultWithPasswordOnLoginSuccess(
|
||||||
unlockVaultWithPasswordOnLoginSuccess(
|
loginResponse = loginResponse,
|
||||||
loginResponse = loginResponse,
|
userStateJson = userStateJson,
|
||||||
userStateJson = userStateJson,
|
password = password,
|
||||||
password = it,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1488,7 +1500,7 @@ class AuthRepositoryImpl(
|
||||||
.hashPassword(
|
.hashPassword(
|
||||||
email = email,
|
email = email,
|
||||||
password = it,
|
password = it,
|
||||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
kdf = profile.toSdkParams(),
|
||||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||||
)
|
)
|
||||||
.onSuccess { passwordHash ->
|
.onSuccess { passwordHash ->
|
||||||
|
@ -1516,8 +1528,11 @@ class AuthRepositoryImpl(
|
||||||
// when we completed the pending admin auth request.
|
// when we completed the pending admin auth request.
|
||||||
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
authDiskSource.storeUserKey(userId = userId, userKey = it)
|
||||||
}
|
}
|
||||||
authDiskSource.storePrivateKey(userId = userId, privateKey = loginResponse.privateKey)
|
loginResponse.privateKey?.let {
|
||||||
|
// Only set the value if it's present, since we may have set it already
|
||||||
|
// when we completed the key connector conversion.
|
||||||
|
authDiskSource.storePrivateKey(userId = userId, privateKey = it)
|
||||||
|
}
|
||||||
// If the user just authenticated with a two-factor code and selected the option to
|
// If the user just authenticated with a two-factor code and selected the option to
|
||||||
// remember it, then the API response will return a token that will be used in place
|
// remember it, then the API response will return a token that will be used in place
|
||||||
// of the two-factor code on the next login attempt.
|
// of the two-factor code on the next login attempt.
|
||||||
|
@ -1566,6 +1581,83 @@ class AuthRepositoryImpl(
|
||||||
return LoginResult.TwoFactorRequired
|
return LoginResult.TwoFactorRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to unlock the current user's vault with key connector data.
|
||||||
|
*/
|
||||||
|
private suspend fun unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||||
|
profile: AccountJson.Profile,
|
||||||
|
keyConnectorUrl: String,
|
||||||
|
orgIdentifier: String,
|
||||||
|
loginResponse: GetTokenResponseJson.Success,
|
||||||
|
): VaultUnlockResult? =
|
||||||
|
if (loginResponse.userDecryptionOptions?.hasMasterPassword != false) {
|
||||||
|
// This user has a master password, so we skip the key-connector logic as it is not
|
||||||
|
// setup yet. The user can still unlock the vault with their master password.
|
||||||
|
null
|
||||||
|
} else if (loginResponse.key != null && loginResponse.privateKey != null) {
|
||||||
|
// This is a returning user who should already have the key connector setup
|
||||||
|
keyConnectorManager
|
||||||
|
.getMasterKeyFromKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = loginResponse.accessToken,
|
||||||
|
)
|
||||||
|
.map {
|
||||||
|
unlockVault(
|
||||||
|
accountProfile = profile,
|
||||||
|
privateKey = loginResponse.privateKey,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = it.masterKey,
|
||||||
|
userKey = loginResponse.key,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
// If the request failed, we want to abort the login process
|
||||||
|
onFailure = { VaultUnlockResult.GenericError },
|
||||||
|
onSuccess = { it },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// This is a new user who needs to setup the key connector
|
||||||
|
keyConnectorManager
|
||||||
|
.migrateNewUserToKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = loginResponse.accessToken,
|
||||||
|
kdfType = loginResponse.kdfType,
|
||||||
|
kdfIterations = loginResponse.kdfIterations,
|
||||||
|
kdfMemory = loginResponse.kdfMemory,
|
||||||
|
kdfParallelism = loginResponse.kdfParallelism,
|
||||||
|
organizationIdentifier = orgIdentifier,
|
||||||
|
)
|
||||||
|
.map { keyConnectorResponse ->
|
||||||
|
val result = unlockVault(
|
||||||
|
accountProfile = profile,
|
||||||
|
privateKey = keyConnectorResponse.keys.private,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = keyConnectorResponse.masterKey,
|
||||||
|
userKey = keyConnectorResponse.encryptedUserKey,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if (result is VaultUnlockResult.Success) {
|
||||||
|
// We now know that login/unlock was successful, so we store the userKey
|
||||||
|
// and privateKey we now have since it didn't exist on the loginResponse
|
||||||
|
authDiskSource.storeUserKey(
|
||||||
|
userId = profile.userId,
|
||||||
|
userKey = keyConnectorResponse.encryptedUserKey,
|
||||||
|
)
|
||||||
|
authDiskSource.storePrivateKey(
|
||||||
|
userId = profile.userId,
|
||||||
|
privateKey = keyConnectorResponse.keys.private,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
.fold(
|
||||||
|
// If the request failed, we want to abort the login process
|
||||||
|
onFailure = { VaultUnlockResult.GenericError },
|
||||||
|
onSuccess = { it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempt to unlock the current user's vault with password data.
|
* Attempt to unlock the current user's vault with password data.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -4,6 +4,7 @@ import app.cash.turbine.test
|
||||||
import com.bitwarden.core.AuthRequestMethod
|
import com.bitwarden.core.AuthRequestMethod
|
||||||
import com.bitwarden.core.AuthRequestResponse
|
import com.bitwarden.core.AuthRequestResponse
|
||||||
import com.bitwarden.core.InitUserCryptoMethod
|
import com.bitwarden.core.InitUserCryptoMethod
|
||||||
|
import com.bitwarden.core.KeyConnectorResponse
|
||||||
import com.bitwarden.core.RegisterKeyResponse
|
import com.bitwarden.core.RegisterKeyResponse
|
||||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||||
import com.bitwarden.core.UpdatePasswordResponse
|
import com.bitwarden.core.UpdatePasswordResponse
|
||||||
|
@ -22,6 +23,7 @@ import com.x8bit.bitwarden.data.auth.datasource.network.model.DeleteAccountRespo
|
||||||
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.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.KeyConnectorMasterKeyResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationAutoEnrollStatusResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationDomainSsoDetailsResponseJson
|
||||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||||
|
@ -2780,6 +2782,432 @@ class AuthRepositoryTest {
|
||||||
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
verify { settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `SSO login get token succeeds with key connector and master password should return success and not unlock the vault`() =
|
||||||
|
runTest {
|
||||||
|
val keyConnectorUrl = "www.example.com"
|
||||||
|
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
userDecryptionOptions = USER_DECRYPTION_OPTIONS.copy(
|
||||||
|
hasMasterPassword = true,
|
||||||
|
trustedDeviceUserDecryptionOptions = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
} returns successResponse.asSuccess()
|
||||||
|
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||||
|
every {
|
||||||
|
successResponse.toUserState(
|
||||||
|
previousUserState = null,
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||||
|
)
|
||||||
|
} returns SINGLE_USER_STATE_1
|
||||||
|
val result = repository.login(
|
||||||
|
email = EMAIL,
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
captchaToken = null,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
assertEquals(LoginResult.Success, result)
|
||||||
|
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||||
|
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = "privateKey")
|
||||||
|
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = "key")
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
vaultRepository.syncIfNecessary()
|
||||||
|
}
|
||||||
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `SSO login get token succeeds with key connector and no master password should return failure`() =
|
||||||
|
runTest {
|
||||||
|
val keyConnectorUrl = "www.example.com"
|
||||||
|
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
userDecryptionOptions = USER_DECRYPTION_OPTIONS.copy(
|
||||||
|
hasMasterPassword = false,
|
||||||
|
trustedDeviceUserDecryptionOptions = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
} returns successResponse.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
keyConnectorManager.getMasterKeyFromKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
)
|
||||||
|
} returns Throwable("Fail").asFailure()
|
||||||
|
every {
|
||||||
|
successResponse.toUserState(
|
||||||
|
previousUserState = null,
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||||
|
)
|
||||||
|
} returns SINGLE_USER_STATE_1
|
||||||
|
|
||||||
|
val result = repository.login(
|
||||||
|
email = EMAIL,
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
captchaToken = null,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LoginResult.Error(errorMessage = null), result)
|
||||||
|
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = null)
|
||||||
|
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
keyConnectorManager.getMasterKeyFromKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `SSO login get token succeeds with key connector and no master password should return success and unlock the vault`() =
|
||||||
|
runTest {
|
||||||
|
val keyConnectorUrl = "www.example.com"
|
||||||
|
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
userDecryptionOptions = USER_DECRYPTION_OPTIONS.copy(
|
||||||
|
hasMasterPassword = false,
|
||||||
|
trustedDeviceUserDecryptionOptions = null,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val masterKey = "masterKey"
|
||||||
|
val keyConnectorMasterKeyResponseJson = mockk<KeyConnectorMasterKeyResponseJson> {
|
||||||
|
every { this@mockk.masterKey } returns masterKey
|
||||||
|
}
|
||||||
|
coEvery {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
} returns successResponse.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
keyConnectorManager.getMasterKeyFromKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
)
|
||||||
|
} returns keyConnectorMasterKeyResponseJson.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVault(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
email = EMAIL,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
privateKey = "privateKey",
|
||||||
|
organizationKeys = null,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = masterKey,
|
||||||
|
userKey = "key",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} returns VaultUnlockResult.Success
|
||||||
|
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||||
|
every {
|
||||||
|
successResponse.toUserState(
|
||||||
|
previousUserState = null,
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||||
|
)
|
||||||
|
} returns SINGLE_USER_STATE_1
|
||||||
|
val result = repository.login(
|
||||||
|
email = EMAIL,
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
captchaToken = null,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LoginResult.Success, result)
|
||||||
|
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||||
|
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = "privateKey")
|
||||||
|
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = "key")
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
keyConnectorManager.getMasterKeyFromKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
)
|
||||||
|
vaultRepository.unlockVault(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
email = EMAIL,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
privateKey = "privateKey",
|
||||||
|
organizationKeys = null,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = masterKey,
|
||||||
|
userKey = "key",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
vaultRepository.syncIfNecessary()
|
||||||
|
}
|
||||||
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `SSO login get token succeeds with key connector, no master password, no key and no private key should return failure`() =
|
||||||
|
runTest {
|
||||||
|
val keyConnectorUrl = "www.example.com"
|
||||||
|
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
userDecryptionOptions = USER_DECRYPTION_OPTIONS.copy(
|
||||||
|
hasMasterPassword = false,
|
||||||
|
trustedDeviceUserDecryptionOptions = null,
|
||||||
|
),
|
||||||
|
key = null,
|
||||||
|
privateKey = null,
|
||||||
|
)
|
||||||
|
coEvery {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
} returns successResponse.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
keyConnectorManager.migrateNewUserToKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
kdfType = PROFILE_1.kdfType!!,
|
||||||
|
kdfIterations = PROFILE_1.kdfIterations,
|
||||||
|
kdfMemory = PROFILE_1.kdfMemory,
|
||||||
|
kdfParallelism = PROFILE_1.kdfParallelism,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
} returns Throwable("Fail").asFailure()
|
||||||
|
every {
|
||||||
|
successResponse.toUserState(
|
||||||
|
previousUserState = null,
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||||
|
)
|
||||||
|
} returns SINGLE_USER_STATE_1
|
||||||
|
|
||||||
|
val result = repository.login(
|
||||||
|
email = EMAIL,
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
captchaToken = null,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LoginResult.Error(errorMessage = null), result)
|
||||||
|
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = null)
|
||||||
|
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = null)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
keyConnectorManager.migrateNewUserToKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
kdfType = PROFILE_1.kdfType!!,
|
||||||
|
kdfIterations = PROFILE_1.kdfIterations,
|
||||||
|
kdfMemory = PROFILE_1.kdfMemory,
|
||||||
|
kdfParallelism = PROFILE_1.kdfParallelism,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
fun `SSO login get token succeeds with key connector, no master password, no key and no private key should return success and unlock the vault`() =
|
||||||
|
runTest {
|
||||||
|
val keyConnectorUrl = "www.example.com"
|
||||||
|
val successResponse = GET_TOKEN_RESPONSE_SUCCESS.copy(
|
||||||
|
keyConnectorUrl = keyConnectorUrl,
|
||||||
|
userDecryptionOptions = USER_DECRYPTION_OPTIONS.copy(
|
||||||
|
hasMasterPassword = false,
|
||||||
|
trustedDeviceUserDecryptionOptions = null,
|
||||||
|
),
|
||||||
|
key = null,
|
||||||
|
privateKey = null,
|
||||||
|
)
|
||||||
|
val masterKey = "masterKey"
|
||||||
|
val keyConnectorResponse = mockk<KeyConnectorResponse> {
|
||||||
|
every {
|
||||||
|
this@mockk.keys
|
||||||
|
} returns RsaKeyPair(public = PUBLIC_KEY, private = PRIVATE_KEY)
|
||||||
|
every { this@mockk.masterKey } returns masterKey
|
||||||
|
every { this@mockk.encryptedUserKey } returns ENCRYPTED_USER_KEY
|
||||||
|
}
|
||||||
|
coEvery {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
} returns successResponse.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
keyConnectorManager.migrateNewUserToKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
kdfType = PROFILE_1.kdfType!!,
|
||||||
|
kdfIterations = PROFILE_1.kdfIterations,
|
||||||
|
kdfMemory = PROFILE_1.kdfMemory,
|
||||||
|
kdfParallelism = PROFILE_1.kdfParallelism,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
} returns keyConnectorResponse.asSuccess()
|
||||||
|
coEvery {
|
||||||
|
vaultRepository.unlockVault(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
email = EMAIL,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
privateKey = PRIVATE_KEY,
|
||||||
|
organizationKeys = null,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = masterKey,
|
||||||
|
userKey = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} returns VaultUnlockResult.Success
|
||||||
|
coEvery { vaultRepository.syncIfNecessary() } just runs
|
||||||
|
every {
|
||||||
|
successResponse.toUserState(
|
||||||
|
previousUserState = null,
|
||||||
|
environmentUrlData = EnvironmentUrlDataJson.DEFAULT_US,
|
||||||
|
)
|
||||||
|
} returns SINGLE_USER_STATE_1
|
||||||
|
val result = repository.login(
|
||||||
|
email = EMAIL,
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
captchaToken = null,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
|
||||||
|
assertEquals(LoginResult.Success, result)
|
||||||
|
assertEquals(AuthState.Authenticated(ACCESS_TOKEN), repository.authStateFlow.value)
|
||||||
|
fakeAuthDiskSource.assertPrivateKey(userId = USER_ID_1, privateKey = PRIVATE_KEY)
|
||||||
|
fakeAuthDiskSource.assertUserKey(userId = USER_ID_1, userKey = ENCRYPTED_USER_KEY)
|
||||||
|
coVerify(exactly = 1) {
|
||||||
|
identityService.getToken(
|
||||||
|
email = EMAIL,
|
||||||
|
authModel = IdentityTokenAuthModel.SingleSignOn(
|
||||||
|
ssoCode = SSO_CODE,
|
||||||
|
ssoCodeVerifier = SSO_CODE_VERIFIER,
|
||||||
|
ssoRedirectUri = SSO_REDIRECT_URI,
|
||||||
|
),
|
||||||
|
captchaToken = null,
|
||||||
|
uniqueAppId = UNIQUE_APP_ID,
|
||||||
|
)
|
||||||
|
keyConnectorManager.migrateNewUserToKeyConnector(
|
||||||
|
url = keyConnectorUrl,
|
||||||
|
accessToken = ACCESS_TOKEN,
|
||||||
|
kdfType = PROFILE_1.kdfType!!,
|
||||||
|
kdfIterations = PROFILE_1.kdfIterations,
|
||||||
|
kdfMemory = PROFILE_1.kdfMemory,
|
||||||
|
kdfParallelism = PROFILE_1.kdfParallelism,
|
||||||
|
organizationIdentifier = ORGANIZATION_IDENTIFIER,
|
||||||
|
)
|
||||||
|
vaultRepository.unlockVault(
|
||||||
|
userId = USER_ID_1,
|
||||||
|
email = EMAIL,
|
||||||
|
kdf = ACCOUNT_1.profile.toSdkParams(),
|
||||||
|
privateKey = "privateKey",
|
||||||
|
organizationKeys = null,
|
||||||
|
initUserCryptoMethod = InitUserCryptoMethod.KeyConnector(
|
||||||
|
masterKey = masterKey,
|
||||||
|
userKey = ENCRYPTED_USER_KEY,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
vaultRepository.syncIfNecessary()
|
||||||
|
}
|
||||||
|
assertEquals(SINGLE_USER_STATE_1, fakeAuthDiskSource.userState)
|
||||||
|
verify(exactly = 1) {
|
||||||
|
settingsRepository.setDefaultsIfNecessary(userId = USER_ID_1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@Suppress("MaxLineLength")
|
@Suppress("MaxLineLength")
|
||||||
fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with UserKey`() =
|
fun `login with device get token succeeds should return Success, update AuthState, update stored keys, and sync with UserKey`() =
|
||||||
|
|
Loading…
Reference in a new issue