Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SDKS-3533 Prevent duplicated notification on iOS SDK #314

Merged
merged 2 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion FRAuthenticator/FRAuthenticator/FRAClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FRAuthenticator.swift
// FRAuthenticator
//
// Copyright (c) 2020-2023 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -199,6 +199,15 @@ public class FRAClient: NSObject {
}


/// Retrieves PushNotification object with given message identifier
/// - Parameter messageId: String value of PushNotification's message identifier
/// - Returns: PushNotification object with given message identifier
/// - Note: If the PushNotification object with given message identifier does not exist, it returns nil
func getNotificationByMessageId(messageId: String) -> PushNotification? {
return self.authenticatorManager.getNotificationByMessageId(messageId: messageId)
}


/// Removes PushNotification object from StorageClient
/// - Parameter notification: PushNotification object to be removed
/// - Returns: boolean result of operation
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// AuthenticatorManager.swift
// FRAuthenticator
//
// Copyright (c) 2020-2023 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -455,6 +455,14 @@ struct AuthenticatorManager {
}


/// Retrieves PushNotification object with given PushNotification Message Identifier
/// - Parameter messageId: String value of PushNotification object's message identifier
/// - Returns: PushNotification object with given message identifier
func getNotificationByMessageId(messageId: String) -> PushNotification? {
return self.storageClient.getNotificationByMessageId(messageId: messageId)
}


/// Removes PushNotification object from StorageClient
/// - Parameter notification: PushNotification object to be removed
/// - Returns: boolean result of operation
Expand Down
61 changes: 34 additions & 27 deletions FRAuthenticator/FRAuthenticator/Push/FRAPushHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FRAPushHandler.swift
// FRAuthenticator
//
// Copyright (c) 2020 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -68,35 +68,42 @@ public class FRAPushHandler: NSObject {

FRALog.v("Received valid format of remote-notification for AM Push Authentication; starts parsing it into PushNotification object")
do {
// Extract JWT payload
FRALog.v("Starts extracting JWT payload: \(jwt)")
let jwtPayload = try FRCompactJWT.extractPayload(jwt: jwt)
FRALog.v("JWT payload is extracted: \(jwtPayload)")

// Construct and save Notification object
FRALog.v("PushNotification object created - messageId:\(messageId), payload: \(jwtPayload)")
let notification = try PushNotification(messageId: messageId, payload: jwtPayload)

if let mechanism = FRAClient.storage.getMechanismForUUID(uuid: notification.mechanismUUID) {
if try FRCompactJWT.verify(jwt: jwt, secret: mechanism.secret) == false {
FRALog.e("Failed to verify given JWT in remote-notification payload; returning nil")
// Check if notification with given messageId already exists
if let notification = FRAClient.storage.getNotificationByMessageId(messageId: messageId) {
FRALog.v("Received remote-notification with messageId: \(messageId) already exists in StorageClient; returning the existing PushNotification object")
return notification
} else {
// Extract JWT payload
FRALog.v("Starts extracting JWT payload: \(jwt)")
let jwtPayload = try FRCompactJWT.extractPayload(jwt: jwt)
FRALog.v("JWT payload is extracted: \(jwtPayload)")

// Construct and save Notification object
FRALog.v("PushNotification object created - messageId:\(messageId), payload: \(jwtPayload)")
let notification = try PushNotification(messageId: messageId, payload: jwtPayload)

if let mechanism = FRAClient.storage.getMechanismForUUID(uuid: notification.mechanismUUID) {
if try FRCompactJWT.verify(jwt: jwt, secret: mechanism.secret) == false {
FRALog.e("Failed to verify given JWT in remote-notification payload; returning nil")
return nil
}
FRALog.v("Verification of JWT in remote-notification payload with PushMechanism's secret")
}
else {
FRALog.e("Failed to retrieve PushMechanism object from StorageClient; returning null")
return nil
}
FRALog.v("Verification of JWT in remote-notification payload with PushMechanism's secret")
}
else {
FRALog.e("Failed to retrieve PushMechanism object from StorageClient; returning null")
return nil
}

if FRAClient.storage.setNotification(notification: notification) {
FRALog.v("PushNotification object is created and saved into StorageClient")
}
else {
FRALog.w("PushNotification object failed to be stored into StorageClient")

if FRAClient.storage.setNotification(notification: notification) {
FRALog.v("PushNotification object is created and saved into StorageClient")
}
else {
FRALog.w("PushNotification object failed to be stored into StorageClient")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we not treat that as an error and return nil? If we end up on this situation, is the Push actionable or would the app be stuck?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a valid reason that the notification will fail the storage step?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch! we should return nil here. It's unlikely we will have an exception while persisting it because the default storage is the Keychain. However, if a developer decide to use SQLite or any other storage, it may happen, so we need to confirm

return nil
}

return notification
}

return notification
}
catch {
FRALog.e("An error occurred during handling incoming PushNotification: \(error.localizedDescription)")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// KeychainServiceStorageClient.swift
// FRAuthenticator
//
// Copyright (c) 2020-2021 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -200,6 +200,29 @@ struct KeychainServiceClient: StorageClient {
}


func getNotificationByMessageId(messageId: String) -> PushNotification? {
if let items = self.notificationStorage.allItems() {
for item in items {
if #available(iOS 11.0, *) {
if let notificationData = item.value as? Data,
let notification = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushNotification.self, from: notificationData),
notification.messageId == messageId {
return notification
}
} else {
if let notificationData = item.value as? Data,
let notification = NSKeyedUnarchiver.unarchiveObject(with: notificationData) as? PushNotification,
notification.messageId == messageId {
return notification
}
}
}
}

return nil
}


@discardableResult func setNotification(notification: PushNotification) -> Bool {
if #available(iOS 11.0, *) {
do {
Expand Down
6 changes: 5 additions & 1 deletion FRAuthenticator/FRAuthenticator/Storage/StorageClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// StorageClient.swift
// FRAuthenticator
//
// Copyright (c) 2020-2021 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -49,6 +49,10 @@ public protocol StorageClient {
/// - Parameter notificationIdentifier: String value of PushNotification's unique identifier
func getNotification(notificationIdentifier: String) -> PushNotification?

/// Retrieves PushNotification object with its unique message identifier
/// - Parameter messageId: String value of PushNotification's message identifier
func getNotificationByMessageId(messageId: String) -> PushNotification?

/// Stores PushNotification object into Storage Client, and returns discardable Boolean result of operation
/// - Parameter notification: PushNotification object to be stored
@discardableResult func setNotification(notification: PushNotification) -> Bool
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FRAClientTests.swift
// FRAuthenticatorTests
//
// Copyright (c) 2020-2023 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -404,6 +404,14 @@ class FRAClientTests: FRABaseTests {
notifications[2].messageId = "AUTHENTICATE:929d72b7-c3e6-4460-a7b6-8e1c950b43361589151096771"
}

// Get push notification by messageId
guard let notification1 = FRAClient.shared?.getNotificationByMessageId(messageId: "AUTHENTICATE:64e909a2-84db-4ee8-b244-f0dbbeb8b0ff1589151035455") else {
XCTFail("Failed to retrieve PushNotification by messageId")
return
}
XCTAssertEqual(notification1.messageId, "AUTHENTICATE:64e909a2-84db-4ee8-b244-f0dbbeb8b0ff1589151035455")


// Remove Account object
// When
guard let fraClient = FRAClient.shared, let pushAccountDelete = FRAClient.shared?.getAccount(identifier: "ForgeRockSandbox-pushtestuser") else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// DummyStorageClient.swift
// FRAuthenticatorTests
//
// Copyright (c) 2020-2021 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -112,6 +112,11 @@ class DummyStorageClient: StorageClient {
}


func getNotificationByMessageId(messageId: String) -> FRAuthenticator.PushNotification? {
return self.defaultStorageClient.getNotificationByMessageId(messageId: messageId)
}


@discardableResult func setNotification(notification: PushNotification) -> Bool {
if let mockResult = self.setNotificationResult {
return mockResult
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// FRAPushHandlerTests.swift
// FRAuthenticatorTests
//
// Copyright (c) 2020 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -189,4 +189,53 @@ class FRAPushHandlerTests: FRABaseTests {
let notification = FRAPushHandler.shared.application(UIApplication.shared, didReceiveRemoteNotification: payload)
XCTAssertNil(notification)
}


func test_11_pushnotification_already_exist() {

// Given
let storage = KeychainServiceClient()
let account = Account(issuer: "Rm9yZ2Vyb2Nr", accountName: "demo")
storage.setAccount(account: account)

let qrCode = URL(string: "pushauth://push/forgerock:demo?a=aHR0cDovL2FtcWEtY2xvbmU2OS50ZXN0LmZvcmdlcm9jay5jb206ODA4MC9vcGVuYW0vanNvbi9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&image=aHR0cDovL3NlYXR0bGV3cml0ZXIuY29tL3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDEzLzAxL3dlaWdodC13YXRjaGVycy1zbWFsbC5naWY&b=ff00ff&r=aHR0cDovL2FtcWEtY2xvbmU2OS50ZXN0LmZvcmdlcm9jay5jb206ODA4MC9vcGVuYW0vanNvbi9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI=&s=dA18Iph3slIUDVuRc5+3y7nv9NLGnPksH66d3jIF6uE=&c=Yf66ojm3Pm80PVvNpljTB6X9CUhgSJ0WZUzB4su3vCY=&l=YW1sYmNvb2tpZT0wMQ==&m=9326d19c-4d08-4538-8151-f8558e71475f1464361288472&issuer=Rm9yZ2Vyb2Nr")!

do {
let parser = try PushQRCodeParser(url: qrCode)
let mechanism = PushMechanism(issuer: parser.issuer, accountName: parser.label, secret: parser.secret, authEndpoint: parser.authenticationEndpoint, regEndpoint: parser.registrationEndpoint, messageId: parser.messageId, challenge: parser.challenge, loadBalancer: parser.loadBalancer)
mechanism.mechanismUUID = "759ACE9D-C64B-43E6-981D-97F7B54C3B01"
FRAClient.storage.setMechanism(mechanism: mechanism)

let payload: [String: String] = ["c": "j4i8MSuGOcqfslLpRMsYWUMkfsZnsgTCcgNZ+WN3MEE=", "l": "ZnJfc3NvX2FtbGJfcHJvZD0wMQ==", "t": "120", "u": mechanism.mechanismUUID, "messageId": "AUTHENTICATE:e84233f8-9ecf-4456-91ad-2649c4103bc01569980570407"]

let messageId = "AUTHENTICATE:e84233f8-9ecf-4456-91ad-2649c4103bc01569980570407"

let notification = try PushNotification(messageId: messageId, payload: payload)

storage.setNotification(notification: notification)
let notifications = storage.getAllNotificationsForMechanism(mechanism: mechanism)

XCTAssertNotNil(notifications)
XCTAssertEqual(notifications.count, 1)
XCTAssertEqual(notifications.first?.messageId, messageId)

var newPayload: [String: Any] = [:]
var aps: [String: Any] = [:]
aps["messageId"] = "AUTHENTICATE:e84233f8-9ecf-4456-91ad-2649c4103bc01569980570407"
aps["content-available"] = true
aps["alert"] = "Login attempt from user at ForgeRockSandbox"
aps["data"] = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjIjoibFltZmVQUzllYisrMWtpbzJJSUpBdHdVV1dDY1pDcytCU2dLUGpaS04yOD0iLCJ0IjoiMTIwIiwidSI6Ijc1OUFDRTlELUM2NEItNDNFNi05ODFELTk3RjdCNTRDM0IwMSIsImwiOiJZVzFzWW1OdmIydHBaVDB3TVE9PSJ9.Kflihn5sCXFQ3TDWe8GBayCinguSLs9nsu4j4JxddtY"
aps["sound"] = "default"
newPayload["aps"] = aps

guard let storedNotification = FRAPushHandler.shared.application(UIApplication.shared, didReceiveRemoteNotification: newPayload) else {
XCTFail("Failed to parse notification payload and construct PushNotification object")
return
}
XCTAssertNotNil(storedNotification)
XCTAssertEqual(storedNotification.messageId, messageId) }
catch {
XCTFail("Failed to parse remote-notification: \(error.localizedDescription)")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// KeychainServiceStorageClientTests.swift
// FRAuthenticatorTests
//
// Copyright (c) 2020-2023 ForgeRock. All rights reserved.
// Copyright (c) 2020-2024 Ping Identity. All rights reserved.
//
// This software may be modified and distributed under the terms
// of the MIT license. See the LICENSE file for details.
Expand Down Expand Up @@ -315,4 +315,40 @@ class KeychainServiceStorageClientTests: FRABaseTests {
// Then
XCTAssertFalse(storage.isEmpty())
}


func test_12_retrieve_notification_by_message_id() {

let storage = KeychainServiceClient()
let account = Account(issuer: "Rm9yZ2Vyb2Nr", accountName: "demo")
storage.setAccount(account: account)

let qrCode = URL(string: "pushauth://push/forgerock:demo?a=aHR0cDovL2FtcWEtY2xvbmU2OS50ZXN0LmZvcmdlcm9jay5jb206ODA4MC9vcGVuYW0vanNvbi9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249YXV0aGVudGljYXRl&image=aHR0cDovL3NlYXR0bGV3cml0ZXIuY29tL3dwLWNvbnRlbnQvdXBsb2Fkcy8yMDEzLzAxL3dlaWdodC13YXRjaGVycy1zbWFsbC5naWY&b=ff00ff&r=aHR0cDovL2FtcWEtY2xvbmU2OS50ZXN0LmZvcmdlcm9jay5jb206ODA4MC9vcGVuYW0vanNvbi9wdXNoL3Nucy9tZXNzYWdlP19hY3Rpb249cmVnaXN0ZXI=&s=dA18Iph3slIUDVuRc5+3y7nv9NLGnPksH66d3jIF6uE=&c=Yf66ojm3Pm80PVvNpljTB6X9CUhgSJ0WZUzB4su3vCY=&l=YW1sYmNvb2tpZT0wMQ==&m=9326d19c-4d08-4538-8151-f8558e71475f1464361288472&issuer=Rm9yZ2Vyb2Nr")!

do {
let parser = try PushQRCodeParser(url: qrCode)
let mechanism = PushMechanism(issuer: parser.issuer, accountName: parser.label, secret: parser.secret, authEndpoint: parser.authenticationEndpoint, regEndpoint: parser.registrationEndpoint, messageId: parser.messageId, challenge: parser.challenge, loadBalancer: parser.loadBalancer)
storage.setMechanism(mechanism: mechanism)

let payload1: [String: String] = ["c": "j4i8MSuGOcqfslLpRMsYWUMkfsZnsgTCcgNZ+WN3MEE=", "l": "ZnJfc3NvX2FtbGJfcHJvZD0wMQ==", "t": "120", "u": mechanism.mechanismUUID]

let messageId1 = "AUTHENTICATE:e84233f8-9ecf-4456-91ad-2649c4103bc01569980570407"

let notification1 = try PushNotification(messageId: messageId1, payload: payload1)

storage.setNotification(notification: notification1)
var notifications = storage.getAllNotificationsForMechanism(mechanism: mechanism)

XCTAssertNotNil(notifications)
XCTAssertEqual(notifications.count, 1)
XCTAssertEqual(notifications.first?.messageId, messageId1)

let storedNotification = storage.getNotificationByMessageId(messageId: messageId1)
XCTAssertNotNil(storedNotification)
XCTAssertEqual(storedNotification?.messageId, messageId1)
}
catch {
XCTFail("Failed with unexpected error: \(error.localizedDescription)")
}
}
}
Loading