Skip to content

Commit

Permalink
SDKS-3533 Prevent duplicated notification on iOS SDK (#314)
Browse files Browse the repository at this point in the history
SDKS-3533 Prevent duplicated notification on iOS SDK
  • Loading branch information
rodrigoareis authored Dec 23, 2024
1 parent cf1485e commit 1b369d9
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 35 deletions.
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")
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)")
}
}
}

0 comments on commit 1b369d9

Please sign in to comment.