mirror of
https://github.com/bitwarden/android.git
synced 2024-10-31 15:15:34 +03:00
Logout a user on sync if the security stamp does not match (#1002)
This commit is contained in:
parent
829934f7c0
commit
d5f8eabf31
8 changed files with 85 additions and 4 deletions
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in a new issue