mirror of
https://github.com/bitwarden/android.git
synced 2024-11-24 18:36:32 +03:00
PM-11642: Security stamp soft logout (#3859)
This commit is contained in:
parent
e3a4a7b153
commit
c017c1b10c
5 changed files with 61 additions and 21 deletions
|
@ -14,15 +14,15 @@ interface UserLogoutManager {
|
||||||
val logoutEventFlow: SharedFlow<LogoutEvent>
|
val logoutEventFlow: SharedFlow<LogoutEvent>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Completely logs out the given [userId], removing all data.
|
* Completely logs out the given [userId], removing all data. If [isExpired] is true, a toast
|
||||||
* If [isExpired] is true, a toast will be displayed
|
* will be displayed letting the user know the session has expired.
|
||||||
* letting the user know the session has expired.
|
|
||||||
*/
|
*/
|
||||||
fun logout(userId: String, isExpired: Boolean = false)
|
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
|
||||||
* the exception of basic account data.
|
* the exception of basic account data. If [isExpired] is true, a toast will be displayed
|
||||||
|
* letting the user know the session has expired.
|
||||||
*/
|
*/
|
||||||
fun softLogout(userId: String)
|
fun softLogout(userId: String, isExpired: Boolean = false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,7 +64,10 @@ class UserLogoutManagerImpl(
|
||||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun softLogout(userId: String) {
|
override fun softLogout(userId: String, isExpired: Boolean) {
|
||||||
|
if (isExpired) {
|
||||||
|
showToast(message = R.string.login_expired)
|
||||||
|
}
|
||||||
authDiskSource.storeAccountTokens(
|
authDiskSource.storeAccountTokens(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
accountTokens = null,
|
accountTokens = null,
|
||||||
|
@ -74,7 +77,11 @@ class UserLogoutManagerImpl(
|
||||||
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
val vaultTimeoutInMinutes = settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||||
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
val vaultTimeoutAction = settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||||
|
|
||||||
switchUserIfAvailable(currentUserId = userId, removeCurrentUserFromAccounts = false)
|
switchUserIfAvailable(
|
||||||
|
currentUserId = userId,
|
||||||
|
removeCurrentUserFromAccounts = false,
|
||||||
|
isExpired = isExpired,
|
||||||
|
)
|
||||||
|
|
||||||
clearData(userId = userId)
|
clearData(userId = userId)
|
||||||
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
mutableLogoutEventFlow.tryEmit(LogoutEvent(loggedOutUserId = userId))
|
||||||
|
|
|
@ -358,7 +358,7 @@ class VaultRepositoryImpl(
|
||||||
// Log the user out if the stamps do not match
|
// Log the user out if the stamps do not match
|
||||||
localSecurityStamp?.let {
|
localSecurityStamp?.let {
|
||||||
if (serverSecurityStamp != localSecurityStamp) {
|
if (serverSecurityStamp != localSecurityStamp) {
|
||||||
userLogoutManager.logout(userId = userId, isExpired = true)
|
userLogoutManager.softLogout(userId = userId, isExpired = true)
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -138,21 +138,21 @@ class UserLogoutManagerTest {
|
||||||
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) }
|
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) }
|
||||||
assertDataCleared(userId = userId)
|
assertDataCleared(userId = userId)
|
||||||
|
|
||||||
verify {
|
verify(exactly = 1) {
|
||||||
settingsDiskSource.storeVaultTimeoutInMinutes(
|
settingsDiskSource.storeVaultTimeoutInMinutes(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
|
vaultTimeoutInMinutes = vaultTimeoutInMinutes,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
verify {
|
|
||||||
settingsDiskSource.storeVaultTimeoutAction(
|
settingsDiskSource.storeVaultTimeoutAction(
|
||||||
userId = userId,
|
userId = userId,
|
||||||
vaultTimeoutAction = vaultTimeoutAction,
|
vaultTimeoutAction = vaultTimeoutAction,
|
||||||
)
|
)
|
||||||
|
Toast
|
||||||
|
.makeText(context, R.string.account_switched_automatically, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Suppress("MaxLineLength")
|
|
||||||
@Test
|
@Test
|
||||||
fun `softLogout should switch active user but keep previous user in accounts list`() {
|
fun `softLogout should switch active user but keep previous user in accounts list`() {
|
||||||
val userId = USER_ID_1
|
val userId = USER_ID_1
|
||||||
|
@ -171,10 +171,44 @@ class UserLogoutManagerTest {
|
||||||
|
|
||||||
userLogoutManager.softLogout(userId = userId)
|
userLogoutManager.softLogout(userId = userId)
|
||||||
|
|
||||||
verify { authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null) }
|
verify(exactly = 1) {
|
||||||
verify {
|
authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null)
|
||||||
authDiskSource.userState =
|
authDiskSource.userState = UserStateJson(
|
||||||
UserStateJson(activeUserId = USER_ID_2, accounts = MULTI_USER_STATE.accounts)
|
activeUserId = USER_ID_2,
|
||||||
|
accounts = MULTI_USER_STATE.accounts,
|
||||||
|
)
|
||||||
|
Toast
|
||||||
|
.makeText(context, R.string.account_switched_automatically, Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("MaxLineLength")
|
||||||
|
@Test
|
||||||
|
fun `softLogout with isExpired true should switch active user and keep previous user in accounts list but display the login expired toast`() {
|
||||||
|
val userId = USER_ID_1
|
||||||
|
val vaultTimeoutInMinutes = 360
|
||||||
|
val vaultTimeoutAction = VaultTimeoutAction.LOGOUT
|
||||||
|
|
||||||
|
mockToast(R.string.login_expired)
|
||||||
|
|
||||||
|
every { authDiskSource.userState } returns MULTI_USER_STATE
|
||||||
|
every {
|
||||||
|
settingsDiskSource.getVaultTimeoutInMinutes(userId = userId)
|
||||||
|
} returns vaultTimeoutInMinutes
|
||||||
|
every {
|
||||||
|
settingsDiskSource.getVaultTimeoutAction(userId = userId)
|
||||||
|
} returns vaultTimeoutAction
|
||||||
|
|
||||||
|
userLogoutManager.softLogout(userId = userId, isExpired = true)
|
||||||
|
|
||||||
|
verify(exactly = 1) {
|
||||||
|
authDiskSource.storeAccountTokens(userId = USER_ID_1, accountTokens = null)
|
||||||
|
authDiskSource.userState = UserStateJson(
|
||||||
|
activeUserId = USER_ID_2,
|
||||||
|
accounts = MULTI_USER_STATE.accounts,
|
||||||
|
)
|
||||||
|
Toast.makeText(context, R.string.login_expired, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ class VaultRepositoryTest {
|
||||||
)
|
)
|
||||||
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
private val dispatcherManager: DispatcherManager = FakeDispatcherManager()
|
||||||
private val userLogoutManager: UserLogoutManager = mockk {
|
private val userLogoutManager: UserLogoutManager = mockk {
|
||||||
every { logout(any(), any()) } just runs
|
every { softLogout(any(), any()) } just runs
|
||||||
}
|
}
|
||||||
private val fileManager: FileManager = mockk {
|
private val fileManager: FileManager = mockk {
|
||||||
coEvery { delete(*anyVararg()) } just runs
|
coEvery { delete(*anyVararg()) } just runs
|
||||||
|
@ -851,9 +851,8 @@ class VaultRepositoryTest {
|
||||||
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
fakeAuthDiskSource.userState = MOCK_USER_STATE
|
||||||
val userId = "mockId-1"
|
val userId = "mockId-1"
|
||||||
val mockSyncResponse = createMockSyncResponse(number = 1)
|
val mockSyncResponse = createMockSyncResponse(number = 1)
|
||||||
coEvery { syncService.sync() } returns mockSyncResponse.copy(
|
coEvery { syncService.sync() } returns mockSyncResponse
|
||||||
profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"),
|
.copy(profile = createMockProfile(number = 1).copy(securityStamp = "newStamp"))
|
||||||
)
|
|
||||||
.asSuccess()
|
.asSuccess()
|
||||||
|
|
||||||
coEvery {
|
coEvery {
|
||||||
|
@ -868,7 +867,7 @@ class VaultRepositoryTest {
|
||||||
vaultRepository.sync()
|
vaultRepository.sync()
|
||||||
|
|
||||||
coVerify {
|
coVerify {
|
||||||
userLogoutManager.logout(userId = userId, isExpired = true)
|
userLogoutManager.softLogout(userId = userId, isExpired = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
coVerify(exactly = 0) {
|
coVerify(exactly = 0) {
|
||||||
|
|
Loading…
Reference in a new issue