mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 02:15:53 +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,
|
||||
environmentUrlData = environmentRepository.environment.environmentUrlData,
|
||||
)
|
||||
val userId = userStateJson.activeUserId
|
||||
val profile = userStateJson.activeAccount.profile
|
||||
val userId = profile.userId
|
||||
|
||||
checkForVaultUnlockError(
|
||||
onVaultUnlockError = { vaultUnlockError ->
|
||||
return@userStateTransaction vaultUnlockError.toLoginErrorResult()
|
||||
},
|
||||
) {
|
||||
val keyConnectorUrl = loginResponse
|
||||
.keyConnectorUrl
|
||||
?: loginResponse
|
||||
.userDecryptionOptions
|
||||
?.keyConnectorUserDecryptionOptions
|
||||
?.keyConnectorUrl
|
||||
val isDeviceUnlockAvailable = deviceData != null ||
|
||||
loginResponse.userDecryptionOptions?.trustedDeviceUserDecryptionOptions != null
|
||||
// if possible attempt to unlock the vault with trusted device data
|
||||
|
@ -1471,14 +1478,19 @@ class AuthRepositoryImpl(
|
|||
userStateJson = userStateJson,
|
||||
deviceData = deviceData,
|
||||
)
|
||||
} else if (keyConnectorUrl != null && orgIdentifier != null) {
|
||||
unlockVaultWithKeyConnectorOnLoginSuccess(
|
||||
profile = profile,
|
||||
keyConnectorUrl = keyConnectorUrl,
|
||||
orgIdentifier = orgIdentifier,
|
||||
loginResponse = loginResponse,
|
||||
)
|
||||
} else {
|
||||
password?.let {
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
password = it,
|
||||
)
|
||||
}
|
||||
unlockVaultWithPasswordOnLoginSuccess(
|
||||
loginResponse = loginResponse,
|
||||
userStateJson = userStateJson,
|
||||
password = password,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1488,7 +1500,7 @@ class AuthRepositoryImpl(
|
|||
.hashPassword(
|
||||
email = email,
|
||||
password = it,
|
||||
kdf = userStateJson.activeAccount.profile.toSdkParams(),
|
||||
kdf = profile.toSdkParams(),
|
||||
purpose = HashPurpose.LOCAL_AUTHORIZATION,
|
||||
)
|
||||
.onSuccess { passwordHash ->
|
||||
|
@ -1516,8 +1528,11 @@ class AuthRepositoryImpl(
|
|||
// when we completed the pending admin auth request.
|
||||
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
|
||||
// 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.
|
||||
|
@ -1566,6 +1581,83 @@ class AuthRepositoryImpl(
|
|||
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.
|
||||
*/
|
||||
|
|
|
@ -4,6 +4,7 @@ import app.cash.turbine.test
|
|||
import com.bitwarden.core.AuthRequestMethod
|
||||
import com.bitwarden.core.AuthRequestResponse
|
||||
import com.bitwarden.core.InitUserCryptoMethod
|
||||
import com.bitwarden.core.KeyConnectorResponse
|
||||
import com.bitwarden.core.RegisterKeyResponse
|
||||
import com.bitwarden.core.RegisterTdeKeyResponse
|
||||
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.IdentityTokenAuthModel
|
||||
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.OrganizationDomainSsoDetailsResponseJson
|
||||
import com.x8bit.bitwarden.data.auth.datasource.network.model.OrganizationKeysResponseJson
|
||||
|
@ -2780,6 +2782,432 @@ class AuthRepositoryTest {
|
|||
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
|
||||
@Suppress("MaxLineLength")
|
||||
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