BIT-1720: logout user when password changed (#470)

This commit is contained in:
Ezimet Ozkhan 2024-02-13 17:32:30 -05:00 committed by GitHub
parent 6b44a67af1
commit 5ecddeec07
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 95 additions and 11 deletions

View File

@ -39,6 +39,10 @@ protocol NotificationService {
/// The delegate to handle login request actions originating from notifications.
///
protocol NotificationServiceDelegate: AnyObject {
/// Users are logged out, route to landing page.
///
func routeToLanding() async
/// Show the login request.
///
/// - Parameter loginRequest: The login request.
@ -68,6 +72,9 @@ class DefaultNotificationService: NotificationService {
/// The service used by the application to manage the app's ID.
private let appIdService: AppIdService
/// The repository used by the application to manage auth data for the UI layer.
private let authRepository: AuthRepository
/// The service used by the application to handle authentication tasks.
private let authService: AuthService
@ -89,6 +96,7 @@ class DefaultNotificationService: NotificationService {
///
/// - Parameters:
/// - appIdService: The service used by the application to manage the app's ID.
/// - authRepository: The repository used by the application to manage auth data for the UI layer.
/// - authService: The service used by the application to handle authentication tasks.
/// - errorReporter: The service used by the application to report non-fatal errors.
/// - notificationAPIService: The API service used to make notification requests.
@ -96,6 +104,7 @@ class DefaultNotificationService: NotificationService {
/// - syncService: The service used to handle syncing vault data with the API.
init(
appIdService: AppIdService,
authRepository: AuthRepository,
authService: AuthService,
errorReporter: ErrorReporter,
notificationAPIService: NotificationAPIService,
@ -103,6 +112,7 @@ class DefaultNotificationService: NotificationService {
syncService: SyncService
) {
self.appIdService = appIdService
self.authRepository = authRepository
self.authService = authService
self.errorReporter = errorReporter
self.notificationAPIService = notificationAPIService
@ -185,8 +195,12 @@ class DefaultNotificationService: NotificationService {
case .syncOrgKeys:
try await syncService.fetchSync(forceSync: true)
case .logOut:
// no-op
break
guard let data: UserNotification = notificationData.data() else { return }
try await authRepository.logout(userId: data.userId)
// Only route to landing page if the current active user was logged out.
if data.userId == userId {
await delegate?.routeToLanding()
}
case .syncSendCreate,
.syncSendUpdate:
if let data: SyncSendNotification = notificationData.data(), data.userId == userId {

View File

@ -6,6 +6,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
// MARK: Properties
var appSettingsStore: MockAppSettingsStore!
var authRepository: MockAuthRepository!
var authService: MockAuthService!
var client: MockHTTPClient!
var delegate: MockNotificationServiceDelegate!
@ -21,6 +22,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
super.setUp()
appSettingsStore = MockAppSettingsStore()
authRepository = MockAuthRepository()
authService = MockAuthService()
client = MockHTTPClient()
delegate = MockNotificationServiceDelegate()
@ -31,6 +33,7 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
subject = DefaultNotificationService(
appIdService: AppIdService(appSettingStore: appSettingsStore),
authRepository: authRepository,
authService: authService,
errorReporter: errorReporter,
notificationAPIService: notificationAPIService,
@ -428,6 +431,54 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
XCTAssertEqual(delegate.showLoginRequestRequest, .fixture())
}
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles logout requests and will not route
/// to the landing screen if the logged-out account was not the currently active account.
func test_messageReceived_logout_nonActiveUser() async throws {
// Set up the mock data.
stateService.setIsAuthenticated()
let activeAccount: Account = .fixture()
let nonActiveAccount: Account = .fixture(profile: .fixture(userId: "b245a33f"))
stateService.accounts = [activeAccount, nonActiveAccount]
let message: [AnyHashable: Any] = [
"aps": [
"data": [
"type": NotificationType.logOut.rawValue,
"payload": "{\"UserId\":\"\(nonActiveAccount.profile.userId)\"}",
],
],
]
// Test.
await subject.messageReceived(message, notificationDismissed: nil, notificationTapped: nil)
XCTAssertEqual(authRepository.logoutUserId, nonActiveAccount.profile.userId)
XCTAssertFalse(delegate.routeToLandingCalled)
}
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles logout requests and will route
/// to the landing screen if the logged-out account was the currently active account.
func test_messageReceived_logout_activeUser() async throws {
// Set up the mock data.
stateService.setIsAuthenticated()
let activeAccount: Account = .fixture()
let nonActiveAccount: Account = .fixture(profile: .fixture(userId: "b245a33f"))
stateService.accounts = [activeAccount, nonActiveAccount]
let message: [AnyHashable: Any] = [
"aps": [
"data": [
"type": NotificationType.logOut.rawValue,
"payload": "{\"UserId\":\"\(activeAccount.profile.userId)\"}",
],
],
]
// Test.
await subject.messageReceived(message, notificationDismissed: nil, notificationTapped: nil)
XCTAssertEqual(authRepository.logoutUserId, activeAccount.profile.userId)
XCTAssertTrue(delegate.routeToLandingCalled)
}
/// `messageReceived(_:notificationDismissed:notificationTapped:)` handles notifications being dismissed.
func test_messageReceived_notificationDismissed() async throws {
// Set up the mock data.
@ -500,12 +551,18 @@ class NotificationServiceTests: BitwardenTestCase { // swiftlint:disable:this ty
// MARK: - MockNotificationServiceDelegate
class MockNotificationServiceDelegate: NotificationServiceDelegate {
var routeToLandingCalled: Bool = false
var showLoginRequestRequest: LoginRequest?
var switchAccountsAccount: Account?
var switchAccountsLoginRequest: LoginRequest?
var switchAccountsShowAlert: Bool?
func routeToLanding() async {
routeToLandingCalled = true
}
func showLoginRequest(_ loginRequest: LoginRequest) {
showLoginRequestRequest = loginRequest
}

View File

@ -331,15 +331,6 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
systemDevice: UIDevice.current
)
let notificationService = DefaultNotificationService(
appIdService: appIdService,
authService: authService,
errorReporter: errorReporter,
notificationAPIService: apiService,
stateService: stateService,
syncService: syncService
)
let authRepository = DefaultAuthRepository(
accountAPIService: apiService,
authService: authService,
@ -354,6 +345,16 @@ public class ServiceContainer: Services { // swiftlint:disable:this type_body_le
vaultTimeoutService: vaultTimeoutService
)
let notificationService = DefaultNotificationService(
appIdService: appIdService,
authRepository: authRepository,
authService: authService,
errorReporter: errorReporter,
notificationAPIService: apiService,
stateService: stateService,
syncService: syncService
)
let generatorRepository = DefaultGeneratorRepository(
clientGenerators: clientService.clientGenerator(),
clientVaultService: clientService.clientVault(),

View File

@ -139,6 +139,12 @@ public class AppProcessor {
}
extension AppProcessor: NotificationServiceDelegate {
/// Users are logged out, route to landing page.
///
func routeToLanding() async {
coordinator?.navigate(to: .auth(.landing))
}
/// Show the login request.
///
/// - Parameter loginRequest: The login request.

View File

@ -112,6 +112,12 @@ class AppProcessorTests: BitwardenTestCase {
XCTAssertEqual(notificationService.messageReceivedMessage?.keys.first, "knock knock")
}
/// `routeToLanding(_:)` navigates to show the landing view.
func test_routeToLanding() async {
await subject.routeToLanding()
XCTAssertEqual(coordinator.routes.last, .auth(.landing))
}
/// Upon a session timeout on app foreground, send the user to the `.didTimeout` route.
func test_shouldSessionTimeout_navigateTo_didTimeout() throws {
let rootNavigator = MockRootNavigator()