diff --git a/forgerock-authenticator/android/build.gradle b/forgerock-authenticator/android/build.gradle index 9ba9e43..0ccdb24 100644 --- a/forgerock-authenticator/android/build.gradle +++ b/forgerock-authenticator/android/build.gradle @@ -58,8 +58,8 @@ dependencies { implementation 'com.google.firebase:firebase-messaging:23.1.2' implementation 'com.google.android.gms:play-services-location:21.0.1' - implementation 'org.forgerock:forgerock-authenticator:4.1.0' - implementation 'org.forgerock:forgerock-core:4.1.0' + implementation 'org.forgerock:forgerock-authenticator:4.8.3' + implementation 'org.forgerock:forgerock-core:4.8.3' //add lib via aar-depency // implementation(name: 'forgerock-authenticator-debug', ext: 'aar') diff --git a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAClientWrapper.java b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAClientWrapper.java index cd45641..821e497 100644 --- a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAClientWrapper.java +++ b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAClientWrapper.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022-2023 ForgeRock. All rights reserved. + * Copyright (c) 2022-2026 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -428,6 +428,25 @@ public void performPushAuthenticationWithBiometric(String notificationId, } } + protected void updateDeviceToken(String newToken) { + if (fraClient == null) { + Log.w(TAG, "FRAClient is not initialized yet. Device token update will be skipped."); + return; + } + + fraClient.updateDeviceToken(newToken, new FRAListener() { + @Override + public void onSuccess(Void result) { + Log.d(TAG, "Device token updated successfully."); + } + + @Override + public void onException(Exception e) { + Log.e(TAG, "Error updating device token.", e); + } + }); + } + private void denyMessage(PushNotification pushNotification, Result flutterResult) { pushNotification.deny(pushAuthenticationListener(pushNotification, flutterResult)); } diff --git a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAMessagingService.java b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAMessagingService.java index 9c7d677..2988cbb 100644 --- a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAMessagingService.java +++ b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAMessagingService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2026 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -23,11 +23,8 @@ public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { public void onNewToken(@NonNull String s) { super.onNewToken(s); - // This FCM method is called if InstanceID token is updated. This may occur if the security - // of the previous token had been compromised. - // Currently OpenAM does not provide an API to receives updates for those tokens. So, there - // is no method available to handle it FRAClient. The current workaround is removed the Push - // mechanism and add it again by scanning a new QRCode. + // Update the device token in the FRAClientWrapper + FRAClientWrapper.getInstanceInBackground(getApplicationContext()).updateDeviceToken(s); } } diff --git a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAStorageClient.java b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAStorageClient.java index f3a17b4..58629fa 100644 --- a/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAStorageClient.java +++ b/forgerock-authenticator/android/src/main/java/org/forgerock/android/auth/FRAStorageClient.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2022 ForgeRock. All rights reserved. + * Copyright (c) 2022-2026 ForgeRock. All rights reserved. * * This software may be modified and distributed under the terms * of the MIT license. See the LICENSE file for details. @@ -10,17 +10,14 @@ import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; -import android.util.Log; import androidx.annotation.NonNull; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.TimeZone; /** * Data Access Object which implements StorageClient interface and uses SecureSharedPreferences from @@ -35,17 +32,23 @@ class FRAStorageClient implements StorageClient { private static final String FORGEROCK_SHARED_PREFERENCES_DATA_ACCOUNT = "com.forgerock.authenticator.DATA.ACCOUNT"; private static final String FORGEROCK_SHARED_PREFERENCES_DATA_MECHANISM = "com.forgerock.authenticator.DATA.MECHANISM"; private static final String FORGEROCK_SHARED_PREFERENCES_DATA_NOTIFICATIONS = "com.forgerock.authenticator.DATA.NOTIFICATIONS"; + private static final String FORGEROCK_SHARED_PREFERENCES_DATA_DEVICE_TOKEN = "com.forgerock.android.authenticator.DATA.DEVICE_TOKEN"; private static final String FORGEROCK_SHARED_PREFERENCES_DATA_BACKUP = "com.forgerock.authenticator.DATA.BACKUP"; + // Device Token key + private static final String DEVICE_TOKEN_ID = "deviceToken"; + //The SharedPreferences to store the data private final SharedPreferences accountData; private final SharedPreferences mechanismData; private final SharedPreferences notificationData; + private final SharedPreferences deviceTokenData; private final SharedPreferences backupData; private final HashMap accountMap; private final HashMap mechanismMap; private final HashMap notificationMap; + private final HashMap deviceTokenMap; private static final String TAG = DefaultStorageClient.class.getSimpleName(); private static final int NOTIFICATIONS_MAX_SIZE = 20; @@ -62,12 +65,15 @@ public FRAStorageClient(Context context) { FORGEROCK_SHARED_PREFERENCES_DATA_MECHANISM, FORGEROCK_SHARED_PREFERENCES_KEYS); this.notificationData = new SecuredSharedPreferences(context, FORGEROCK_SHARED_PREFERENCES_DATA_NOTIFICATIONS, FORGEROCK_SHARED_PREFERENCES_KEYS); + this.deviceTokenData = new SecuredSharedPreferences(context, + FORGEROCK_SHARED_PREFERENCES_DATA_DEVICE_TOKEN, FORGEROCK_SHARED_PREFERENCES_KEYS); this.backupData = new SecuredSharedPreferences(context, FORGEROCK_SHARED_PREFERENCES_DATA_BACKUP, FORGEROCK_SHARED_PREFERENCES_KEYS); this.accountMap = new HashMap<>(); this.mechanismMap = new HashMap<>(); this.notificationMap = new HashMap<>(); + this.deviceTokenMap = new HashMap<>(); } @Override @@ -325,7 +331,7 @@ public boolean removeNotification(PushNotification pushNotification) { public void removeAllNotifications() { notificationData.edit() .clear() - .commit(); + .apply(); notificationMap.clear(); } @@ -343,6 +349,45 @@ public PushNotification getNotification(String notificationId) { } } + @Override + public PushNotification getNotificationByMessageId(String s) { + if(this.notificationMap.isEmpty()) { + this.getAllNotifications(); + } + + for (PushNotification pushNotification : this.notificationMap.values()) { + if (pushNotification.getMessageId().equals(s)) { + return pushNotification; + } + } + + return null; + } + + @Override + public boolean setPushDeviceToken(PushDeviceToken pushDeviceToken) { + String pushDeviceTokenJson = pushDeviceToken.serialize(); + boolean success = deviceTokenData.edit() + .putString(DEVICE_TOKEN_ID, pushDeviceTokenJson) + .commit(); + + if(success) { + this.deviceTokenMap.put(DEVICE_TOKEN_ID, pushDeviceToken); + } + + return success; + } + + @Override + public PushDeviceToken getPushDeviceToken() { + if(this.deviceTokenMap.containsKey(DEVICE_TOKEN_ID)) { + return this.deviceTokenMap.get(DEVICE_TOKEN_ID); + } else { + String json = deviceTokenData.getString(DEVICE_TOKEN_ID, null); + return PushDeviceToken.deserialize(json); + } + } + @Override public boolean setNotification(@NonNull PushNotification pushNotification) { String notificationJson = pushNotification.serialize(); @@ -366,7 +411,8 @@ public boolean setNotification(@NonNull PushNotification pushNotification) { public boolean isEmpty() { return accountData.getAll().isEmpty() && mechanismData.getAll().isEmpty() && - notificationData.getAll().isEmpty(); + notificationData.getAll().isEmpty() && + deviceTokenData.getAll().isEmpty(); } /** diff --git a/forgerock-authenticator/ios/Classes/FRAStorageClient.swift b/forgerock-authenticator/ios/Classes/FRAStorageClient.swift index ec0ab22..3795171 100644 --- a/forgerock-authenticator/ios/Classes/FRAStorageClient.swift +++ b/forgerock-authenticator/ios/Classes/FRAStorageClient.swift @@ -1,5 +1,5 @@ // -// Copyright (c) 2022-2023 ForgeRock. All rights reserved. +// Copyright (c) 2022-2026 ForgeRock. All rights reserved. // // This software may be modified and distributed under the terms // of the MIT license. See the LICENSE file for details. @@ -11,17 +11,22 @@ import FRAuthenticator struct FRAStorageClient: StorageClient { + /// The key identifier for storing device token in Keychain + let deviceTokenIdentifier = "deviceToken" + /// Keychain Service types for all storages in SDK enum KeychainStoreType: String { case account = ".account" case mechanism = ".mechanism" case notification = ".notification" + case pushDeviceToken = ".pushDeviceToken" case backup = ".backup" } var accountStorage: KeychainService var mechanismStorage: KeychainService var notificationStorage: KeychainService + var pushDeviceTokenStorage: KeychainService var backupStorage: KeychainService let keychainServiceIdentifier = "com.forgerock.authenticator.keychainservice.local" let notificationsMaxSize = 20 @@ -30,23 +35,19 @@ struct FRAStorageClient: StorageClient { self.accountStorage = KeychainService(service: keychainServiceIdentifier + KeychainStoreType.account.rawValue) self.mechanismStorage = KeychainService(service: keychainServiceIdentifier + KeychainStoreType.mechanism.rawValue) self.notificationStorage = KeychainService(service: keychainServiceIdentifier + KeychainStoreType.notification.rawValue) + self.pushDeviceTokenStorage = KeychainService(service: keychainServiceIdentifier + KeychainStoreType.pushDeviceToken.rawValue) self.backupStorage = KeychainService(service: keychainServiceIdentifier + KeychainStoreType.backup.rawValue) } @discardableResult func setAccount(account: Account) -> Bool { - if #available(iOS 11.0, *) { - do { - let accountData = try NSKeyedArchiver.archivedData(withRootObject: account, requiringSecureCoding: true) - return self.accountStorage.set(accountData, key: account.identifier) - } - catch { - FRALog.e("Failed to serialize Account object: \(error.localizedDescription)") - return false - } - } else { - let accountData = NSKeyedArchiver.archivedData(withRootObject: account) + do { + let accountData = try NSKeyedArchiver.archivedData(withRootObject: account, requiringSecureCoding: true) return self.accountStorage.set(accountData, key: account.identifier) } + catch { + FRALog.e("Failed to serialize Account object: \(error.localizedDescription)") + return false + } } @discardableResult func removeAccount(account: Account) -> Bool { @@ -54,8 +55,8 @@ struct FRAStorageClient: StorageClient { } func getAccount(accountIdentifier: String) -> Account? { - if let accountData = self.accountStorage.getData(accountIdentifier), - let account = NSKeyedUnarchiver.unarchiveObject(with: accountData) as? Account { + guard let accountData = self.accountStorage.getData(accountIdentifier) else { return nil } + if let account = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Account.self, from: accountData) { return account } else { @@ -67,7 +68,7 @@ struct FRAStorageClient: StorageClient { var accounts: [Account] = [] if let items = self.accountStorage.allItems() { for item in items { - if let accountData = item.value as? Data, let account = NSKeyedUnarchiver.unarchiveObject(with: accountData) as? Account { + if let accountData = item.value as? Data, let account = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Account.self, from: accountData) { accounts.append(account) } } @@ -78,19 +79,14 @@ struct FRAStorageClient: StorageClient { } @discardableResult func setMechanism(mechanism: Mechanism) -> Bool { - if #available(iOS 11.0, *) { - do { - let mechanismData = try NSKeyedArchiver.archivedData(withRootObject: mechanism, requiringSecureCoding: true) - return self.mechanismStorage.set(mechanismData, key: mechanism.identifier) - } - catch { - FRALog.e("Failed to serialize Mechanism object: \(error.localizedDescription)") - return false - } - } else { - let mechanismData = NSKeyedArchiver.archivedData(withRootObject: mechanism) + do { + let mechanismData = try NSKeyedArchiver.archivedData(withRootObject: mechanism, requiringSecureCoding: true) return self.mechanismStorage.set(mechanismData, key: mechanism.identifier) } + catch { + FRALog.e("Failed to serialize Mechanism object: \(error.localizedDescription)") + return false + } } @discardableResult func removeMechanism(mechanism: Mechanism) -> Bool { @@ -102,7 +98,7 @@ struct FRAStorageClient: StorageClient { if let items = self.mechanismStorage.allItems() { for item in items { if let mechanismData = item.value as? Data, - let mechanism = NSKeyedUnarchiver.unarchiveObject(with: mechanismData) as? Mechanism { + let mechanism = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Mechanism.self, from: mechanismData) { if mechanism.issuer == account.issuer && mechanism.accountName == account.accountName { mechanisms.append(mechanism) } @@ -119,7 +115,7 @@ struct FRAStorageClient: StorageClient { if let items = self.mechanismStorage.allItems() { for item in items { if let mechanismData = item.value as? Data, - let mechanism = NSKeyedUnarchiver.unarchiveObject(with: mechanismData) as? Mechanism { + let mechanism = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Mechanism.self, from: mechanismData) { mechanisms.append(mechanism) } } @@ -130,7 +126,7 @@ struct FRAStorageClient: StorageClient { func getMechanism(mechanismIdentifier: String) -> Mechanism? { let id = getMechanismId(mechanismId: mechanismIdentifier) if let mechanismData = self.mechanismStorage.getData(id), - let mechanism = NSKeyedUnarchiver.unarchiveObject(with: mechanismData) as? Mechanism { + let mechanism = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Mechanism.self, from: mechanismData) { return mechanism } else { @@ -146,7 +142,7 @@ struct FRAStorageClient: StorageClient { if let items = self.mechanismStorage.allItems() { for item in items { if let mechanismData = item.value as? Data, - let mechanism = NSKeyedUnarchiver.unarchiveObject(with: mechanismData) as? Mechanism { + let mechanism = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Mechanism.self, from: mechanismData) { if mechanism.mechanismUUID == uuid { return mechanism } @@ -161,7 +157,7 @@ struct FRAStorageClient: StorageClient { if let items = self.mechanismStorage.allItems() { for item in items { if let mechanismData = item.value as? Data, - let mechanism = NSKeyedUnarchiver.unarchiveObject(with: mechanismData) as? Mechanism { + let mechanism = try? NSKeyedUnarchiver.unarchivedObject(ofClass: Mechanism.self, from: mechanismData) { mechanismMap[mechanism.mechanismUUID] = MechanismConverter.toJson(mechanism:mechanism) } } @@ -170,19 +166,14 @@ struct FRAStorageClient: StorageClient { } @discardableResult func setNotification(notification: PushNotification) -> Bool { - if #available(iOS 11.0, *) { - do { - let notificationData = try NSKeyedArchiver.archivedData(withRootObject: notification, requiringSecureCoding: true) - return self.notificationStorage.set(notificationData, key: notification.identifier) - } - catch { - FRALog.e("Failed to serialize PushNotification object: \(error.localizedDescription)") - return false - } - } else { - let notificationData = NSKeyedArchiver.archivedData(withRootObject: notification) + do { + let notificationData = try NSKeyedArchiver.archivedData(withRootObject: notification, requiringSecureCoding: true) return self.notificationStorage.set(notificationData, key: notification.identifier) } + catch { + FRALog.e("Failed to serialize PushNotification object: \(error.localizedDescription)") + return false + } } @discardableResult func removeNotification(notification: PushNotification) -> Bool { @@ -194,8 +185,8 @@ struct FRAStorageClient: StorageClient { } func getNotification(notificationIdentifier: String) -> PushNotification? { - if let notificationData = self.notificationStorage.getData(notificationIdentifier), - let notification = NSKeyedUnarchiver.unarchiveObject(with: notificationData) as? PushNotification { + guard let notificationData = self.notificationStorage.getData(notificationIdentifier) else { return nil } + if let notification = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushNotification.self, from: notificationData) { return notification } else { @@ -208,7 +199,7 @@ struct FRAStorageClient: StorageClient { if let items = self.notificationStorage.allItems() { for item in items { if let notificationData = item.value as? Data, - let notification = NSKeyedUnarchiver.unarchiveObject(with: notificationData) as? PushNotification, + let notification = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushNotification.self, from: notificationData), notification.mechanismUUID == mechanism.mechanismUUID { notifications.append(notification) } @@ -223,7 +214,7 @@ struct FRAStorageClient: StorageClient { var notifications: [PushNotification] = [] if let items = self.notificationStorage.allItems() { for item in items { - if let notificationData = item.value as? Data, let notification = NSKeyedUnarchiver.unarchiveObject(with: notificationData) as? PushNotification { + if let notificationData = item.value as? Data, let notification = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushNotification.self, from: notificationData) { notifications.append(notification) } } @@ -234,6 +225,20 @@ struct FRAStorageClient: StorageClient { return self.removeOldNotificationEntries(notifications: ¬ifications) } + func getNotificationByMessageId(messageId: String) -> PushNotification? { + if let items = self.notificationStorage.allItems() { + for item in items { + if let notificationData = item.value as? Data, + let notification = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushNotification.self, from: notificationData), + notification.messageId == messageId { + return notification + } + } + } + + return nil + } + private func removeOldNotificationEntries(notifications: inout [PushNotification]) -> [PushNotification] { FRALog.v("Checking old PushNotification entries to remove...") var removedEntries = 0 @@ -246,8 +251,38 @@ struct FRAStorageClient: StorageClient { return notifications } + @discardableResult func setPushDeviceToken(pushDeviceToken: PushDeviceToken) -> Bool { + do { + let pushDeviceTokenData = try NSKeyedArchiver.archivedData(withRootObject: pushDeviceToken, requiringSecureCoding: true) + return self.pushDeviceTokenStorage.set(pushDeviceTokenData, key: deviceTokenIdentifier) + } + catch { + FRALog.e("Failed to serialize PushDeviceToken object: \(error.localizedDescription)") + return false + } + } + + + func getPushDeviceToken() -> PushDeviceToken? { + guard let pushDeviceTokenData = self.pushDeviceTokenStorage.getData(deviceTokenIdentifier) else { return nil } + if let pushDeviceToken = try? NSKeyedUnarchiver.unarchivedObject(ofClass: PushDeviceToken.self, from: pushDeviceTokenData) { + return pushDeviceToken + } + else { + return nil + } + } + + + @discardableResult func removePushDeviceToken() -> Bool { + return self.pushDeviceTokenStorage.delete(deviceTokenIdentifier) + } + @discardableResult func isEmpty() -> Bool { - return self.notificationStorage.allItems()?.count == 0 && self.mechanismStorage.allItems()?.count == 0 && self.accountStorage.allItems()?.count == 0 + return self.notificationStorage.allItems()?.count == 0 && + self.mechanismStorage.allItems()?.count == 0 && + self.accountStorage.allItems()?.count == 0 && + self.pushDeviceTokenStorage.allItems()?.count == 0 } func removeAllData() { @@ -267,8 +302,8 @@ struct FRAStorageClient: StorageClient { func getBackup(identifier: String) -> String? { if let backupData = self.backupStorage.getData(identifier), - let data = NSKeyedUnarchiver.unarchiveObject(with: backupData) as? String { - return data + let data = try? NSKeyedUnarchiver.unarchivedObject(ofClass: NSString.self, from: backupData) { + return data as String } else { return nil @@ -276,19 +311,14 @@ struct FRAStorageClient: StorageClient { } @discardableResult func setBackup(identifier: String, jsonData: String) -> Bool { - if #available(iOS 11.0, *) { - do { - let backupData = try NSKeyedArchiver.archivedData(withRootObject: jsonData, requiringSecureCoding: true) - return self.backupStorage.set(backupData, key: identifier) - } - catch { - FRALog.e("Failed to serialize String object: \(error.localizedDescription)") - return false - } - } else { - let backupData = NSKeyedArchiver.archivedData(withRootObject: jsonData) + do { + let backupData = try NSKeyedArchiver.archivedData(withRootObject: jsonData as NSString, requiringSecureCoding: true) return self.backupStorage.set(backupData, key: identifier) } + catch { + FRALog.e("Failed to serialize String object: \(error.localizedDescription)") + return false + } } } diff --git a/forgerock-authenticator/ios/forgerock_authenticator.podspec b/forgerock-authenticator/ios/forgerock_authenticator.podspec index f6ff942..6f3526c 100644 --- a/forgerock-authenticator/ios/forgerock_authenticator.podspec +++ b/forgerock-authenticator/ios/forgerock_authenticator.podspec @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.platform = :ios, '12.0' s.dependency 'Flutter' - s.dependency 'FRAuthenticator', '4.1.1-beta1' + s.dependency 'FRAuthenticator', '4.8.4' # Flutter.framework does not contain a i386 slice. s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }