Logout a user on sync if the security stamp does not match (#1002)

This commit is contained in:
Oleg Semenenko 2024-02-13 17:36:37 -06:00 committed by Álison Fernandes
parent 829934f7c0
commit d5f8eabf31
8 changed files with 85 additions and 4 deletions

View file

@ -6,8 +6,10 @@ package com.x8bit.bitwarden.data.auth.manager
interface UserLogoutManager { interface UserLogoutManager {
/** /**
* Completely logs out the given [userId], removing all data. * Completely logs out the given [userId], removing all data.
* If [isExpired] is true, a toast will be displayed
* letting the user know the session has expired.
*/ */
fun logout(userId: String) fun logout(userId: String, isExpired: Boolean = false)
/** /**
* Partially logs out the given [userId]. All data for the given [userId] will be removed with * Partially logs out the given [userId]. All data for the given [userId] will be removed with

View file

@ -1,5 +1,8 @@
package com.x8bit.bitwarden.data.auth.manager package com.x8bit.bitwarden.data.auth.manager
import android.content.Context
import android.widget.Toast
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.PushDiskSource
@ -16,6 +19,7 @@ import kotlinx.coroutines.launch
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class UserLogoutManagerImpl( class UserLogoutManagerImpl(
private val context: Context,
private val authDiskSource: AuthDiskSource, private val authDiskSource: AuthDiskSource,
private val generatorDiskSource: GeneratorDiskSource, private val generatorDiskSource: GeneratorDiskSource,
private val passwordHistoryDiskSource: PasswordHistoryDiskSource, private val passwordHistoryDiskSource: PasswordHistoryDiskSource,
@ -25,10 +29,17 @@ class UserLogoutManagerImpl(
private val dispatcherManager: DispatcherManager, private val dispatcherManager: DispatcherManager,
) : UserLogoutManager { ) : UserLogoutManager {
private val scope = CoroutineScope(dispatcherManager.unconfined) private val scope = CoroutineScope(dispatcherManager.unconfined)
private val mainScope = CoroutineScope(dispatcherManager.main)
override fun logout(userId: String) { override fun logout(userId: String, isExpired: Boolean) {
val currentUserState = authDiskSource.userState ?: return val currentUserState = authDiskSource.userState ?: return
if (isExpired) {
mainScope.launch {
Toast.makeText(context, R.string.login_expired, Toast.LENGTH_SHORT).show()
}
}
// Remove the active user from the accounts map // Remove the active user from the accounts map
val updatedAccounts = currentUserState val updatedAccounts = currentUserState
.accounts .accounts

View file

@ -45,6 +45,7 @@ object AuthManagerModule {
@Provides @Provides
@Singleton @Singleton
fun provideUserLogoutManager( fun provideUserLogoutManager(
@ApplicationContext context: Context,
authDiskSource: AuthDiskSource, authDiskSource: AuthDiskSource,
generatorDiskSource: GeneratorDiskSource, generatorDiskSource: GeneratorDiskSource,
passwordHistoryDiskSource: PasswordHistoryDiskSource, passwordHistoryDiskSource: PasswordHistoryDiskSource,
@ -54,6 +55,7 @@ object AuthManagerModule {
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
): UserLogoutManager = ): UserLogoutManager =
UserLogoutManagerImpl( UserLogoutManagerImpl(
context = context,
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource, generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource, passwordHistoryDiskSource = passwordHistoryDiskSource,

View file

@ -13,6 +13,7 @@ import com.bitwarden.core.SendType
import com.bitwarden.core.SendView import com.bitwarden.core.SendView
import com.bitwarden.crypto.Kdf import com.bitwarden.crypto.Kdf
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson import com.x8bit.bitwarden.data.auth.repository.util.toUpdatedUserStateJson
import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow import com.x8bit.bitwarden.data.auth.repository.util.userSwitchingChangesFlow
@ -139,6 +140,7 @@ class VaultRepositoryImpl(
private val fileManager: FileManager, private val fileManager: FileManager,
private val vaultLockManager: VaultLockManager, private val vaultLockManager: VaultLockManager,
private val totpCodeManager: TotpCodeManager, private val totpCodeManager: TotpCodeManager,
private val userLogoutManager: UserLogoutManager,
private val pushManager: PushManager, private val pushManager: PushManager,
private val clock: Clock, private val clock: Clock,
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
@ -305,6 +307,7 @@ class VaultRepositoryImpl(
} }
} }
@Suppress("LongMethod")
override fun sync() { override fun sync() {
val userId = activeUserId ?: return val userId = activeUserId ?: return
if (!syncJob.isCompleted) return if (!syncJob.isCompleted) return
@ -318,6 +321,18 @@ class VaultRepositoryImpl(
.sync() .sync()
.fold( .fold(
onSuccess = { syncResponse -> onSuccess = { syncResponse ->
val localSecurityStamp =
authDiskSource.userState?.activeAccount?.profile?.stamp
val serverSecurityStamp = syncResponse.profile.securityStamp
// Log the user out if the stamps do not match
localSecurityStamp?.let {
if (serverSecurityStamp != localSecurityStamp) {
userLogoutManager.logout(userId = userId, isExpired = true)
return@launch
}
}
// Update user information with additional information from sync response // Update user information with additional information from sync response
authDiskSource.userState = authDiskSource authDiskSource.userState = authDiskSource
.userState .userState

View file

@ -1,6 +1,7 @@
package com.x8bit.bitwarden.data.vault.repository.di package com.x8bit.bitwarden.data.vault.repository.di
import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource import com.x8bit.bitwarden.data.auth.datasource.disk.AuthDiskSource
import com.x8bit.bitwarden.data.auth.manager.UserLogoutManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
import com.x8bit.bitwarden.data.platform.manager.PushManager import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
@ -45,6 +46,7 @@ object VaultRepositoryModule {
dispatcherManager: DispatcherManager, dispatcherManager: DispatcherManager,
totpCodeManager: TotpCodeManager, totpCodeManager: TotpCodeManager,
pushManager: PushManager, pushManager: PushManager,
userLogoutManager: UserLogoutManager,
clock: Clock, clock: Clock,
): VaultRepository = VaultRepositoryImpl( ): VaultRepository = VaultRepositoryImpl(
syncService = syncService, syncService = syncService,
@ -60,6 +62,7 @@ object VaultRepositoryModule {
dispatcherManager = dispatcherManager, dispatcherManager = dispatcherManager,
totpCodeManager = totpCodeManager, totpCodeManager = totpCodeManager,
pushManager = pushManager, pushManager = pushManager,
userLogoutManager = userLogoutManager,
clock = clock, clock = clock,
) )
} }

View file

@ -45,6 +45,7 @@ class UserLogoutManagerTest {
private val userLogoutManager: UserLogoutManager = private val userLogoutManager: UserLogoutManager =
UserLogoutManagerImpl( UserLogoutManagerImpl(
context = mockk(),
authDiskSource = authDiskSource, authDiskSource = authDiskSource,
generatorDiskSource = generatorDiskSource, generatorDiskSource = generatorDiskSource,
passwordHistoryDiskSource = passwordHistoryDiskSource, passwordHistoryDiskSource = passwordHistoryDiskSource,

View file

@ -195,7 +195,7 @@ class AuthRepositoryTest {
} returns "AsymmetricEncString".asSuccess() } returns "AsymmetricEncString".asSuccess()
} }
private val userLogoutManager: UserLogoutManager = mockk { private val userLogoutManager: UserLogoutManager = mockk {
every { logout(any()) } just runs every { logout(any(), any()) } just runs
} }
private val mutableLogoutFlow = bufferedMutableSharedFlow<Unit>() private val mutableLogoutFlow = bufferedMutableSharedFlow<Unit>()

View file

@ -18,6 +18,7 @@ import com.bitwarden.core.TotpResponse
import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson import com.x8bit.bitwarden.data.auth.datasource.disk.model.AccountJson
import com.x8bit.bitwarden.data.auth.datasource.disk.model.UserStateJson 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.manager.UserLogoutManager
import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams import com.x8bit.bitwarden.data.auth.repository.util.toSdkParams
import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager import com.x8bit.bitwarden.data.platform.base.FakeDispatcherManager
import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource import com.x8bit.bitwarden.data.platform.datasource.disk.SettingsDiskSource
@ -56,6 +57,7 @@ import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockFolder
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganization
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockOrganizationKeys
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockPolicy
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockProfile
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSend
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSendJsonRequest
import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse import com.x8bit.bitwarden.data.vault.datasource.network.model.createMockSyncResponse
@ -148,6 +150,9 @@ class VaultRepositoryTest {
ZoneOffset.UTC, ZoneOffset.UTC,
) )
private val dispatcherManager: DispatcherManager = FakeDispatcherManager() private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
private val userLogoutManager: UserLogoutManager = mockk() {
every { logout(any(), any()) } just runs
}
private val fileManager: FileManager = mockk() private val fileManager: FileManager = mockk()
private val fakeAuthDiskSource = FakeAuthDiskSource() private val fakeAuthDiskSource = FakeAuthDiskSource()
private val settingsDiskSource = mockk<SettingsDiskSource>() private val settingsDiskSource = mockk<SettingsDiskSource>()
@ -215,6 +220,7 @@ class VaultRepositoryTest {
pushManager = pushManager, pushManager = pushManager,
fileManager = fileManager, fileManager = fileManager,
clock = clock, clock = clock,
userLogoutManager = userLogoutManager,
) )
@BeforeEach @BeforeEach
@ -713,6 +719,47 @@ class VaultRepositoryTest {
} }
} }
@Suppress("MaxLineLength")
@Test
fun `sync with syncService Success with a different security stamp should logout and return early`() =
runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE
val userId = "mockId-1"
val mockSyncResponse = createMockSyncResponse(number = 1)
coEvery { syncService.sync() } returns mockSyncResponse.copy(
profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"),
)
.asSuccess()
coEvery {
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
} returns InitializeCryptoResult.Success.asSuccess()
vaultRepository.sync()
coVerify {
userLogoutManager.logout(userId = userId, isExpired = true)
}
coVerify(exactly = 0) {
vaultDiskSource.replaceVaultData(
userId = MOCK_USER_STATE.activeUserId,
vault = any(),
)
vaultSdkSource.initializeOrganizationCrypto(
userId = userId,
request = InitOrgCryptoRequest(
organizationKeys = createMockOrganizationKeys(1),
),
)
}
}
@Test @Test
fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest { fun `sync with syncService Failure should update DataStateFlow with an Error`() = runTest {
fakeAuthDiskSource.userState = MOCK_USER_STATE fakeAuthDiskSource.userState = MOCK_USER_STATE
@ -5456,7 +5503,7 @@ private val MOCK_PROFILE = AccountJson.Profile(
email = "email", email = "email",
isEmailVerified = true, isEmailVerified = true,
name = null, name = null,
stamp = null, stamp = "mockSecurityStamp-1",
organizationId = null, organizationId = null,
avatarColorHex = null, avatarColorHex = null,
hasPremium = false, hasPremium = false,