diff --git a/FRAuth/FRAuth.xcodeproj/project.pbxproj b/FRAuth/FRAuth.xcodeproj/project.pbxproj index 110dba78..f4342cdd 100644 --- a/FRAuth/FRAuth.xcodeproj/project.pbxproj +++ b/FRAuth/FRAuth.xcodeproj/project.pbxproj @@ -26,6 +26,7 @@ 3A67B8832AD83946003331C5 /* FRAppIntegrityKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */; }; 3A6D26672A1345400099D877 /* PolicyAdviceCreator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A6D26662A1345400099D877 /* PolicyAdviceCreator.swift */; }; 3AB062FA2AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */; }; + 9527F63F2CA32942008475B7 /* AA_12_ReCaptchaEnterpriseCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9527F63E2CA32942008475B7 /* AA_12_ReCaptchaEnterpriseCallbackTest.swift */; }; 9536C56C2B865DD600B2DFDD /* AA_08_TextInputCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9536C56B2B865DD600B2DFDD /* AA_08_TextInputCallbackTest.swift */; }; 959D7D98290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */; }; 95A812F02C516FC4001CDFCB /* AA_11_TextOutputCallbackTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95A812EF2C516FC4001CDFCB /* AA_11_TextOutputCallbackTest.swift */; }; @@ -363,6 +364,7 @@ 3A67B8822AD83946003331C5 /* FRAppIntegrityKeys.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeys.swift; sourceTree = ""; }; 3A6D26662A1345400099D877 /* PolicyAdviceCreator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PolicyAdviceCreator.swift; sourceTree = ""; }; 3AB062F92AE6224D00C4B47C /* FRAppIntegrityKeysTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRAppIntegrityKeysTests.swift; sourceTree = ""; }; + 9527F63E2CA32942008475B7 /* AA_12_ReCaptchaEnterpriseCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AA_12_ReCaptchaEnterpriseCallbackTest.swift; sourceTree = ""; }; 9536C56B2B865DD600B2DFDD /* AA_08_TextInputCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AA_08_TextInputCallbackTest.swift; sourceTree = ""; }; 959D7D97290B4B9200A1F22F /* AA-05-DeviceBindingCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AA-05-DeviceBindingCallbackTest.swift"; sourceTree = ""; }; 95A812EF2C516FC4001CDFCB /* AA_11_TextOutputCallbackTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AA_11_TextOutputCallbackTest.swift; sourceTree = ""; }; @@ -1223,6 +1225,7 @@ 95EB7E4C2B8D010B00B59CD6 /* AA_09_PingOneProtectInitializeCallbackTest.swift */, 95EB7E522B8D5F6100B59CD6 /* AA_10_PingOneProtectEvaluateCallbackTest.swift */, 95A812EF2C516FC4001CDFCB /* AA_11_TextOutputCallbackTest.swift */, + 9527F63E2CA32942008475B7 /* AA_12_ReCaptchaEnterpriseCallbackTest.swift */, ); path = "Callback-Live"; sourceTree = ""; @@ -1975,6 +1978,7 @@ D5791BD125F87DE8004B487A /* FRDeviceCollectorTests.swift in Sources */, D5791BD225F87DE8004B487A /* AuthErrorTests.swift in Sources */, D5791BD325F87DE8004B487A /* ConfigErrorTests.swift in Sources */, + 9527F63F2CA32942008475B7 /* AA_12_ReCaptchaEnterpriseCallbackTest.swift in Sources */, D5791BD425F87DE8004B487A /* TokenErrorTests.swift in Sources */, D5791BD525F87DE8004B487A /* AuthApiErrorTests.swift in Sources */, D5791BD625F87DE8004B487A /* BrowserErrorTests.swift in Sources */, diff --git a/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_12_ReCaptchaEnterpriseCallbackTest.swift b/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_12_ReCaptchaEnterpriseCallbackTest.swift new file mode 100644 index 00000000..023774f3 --- /dev/null +++ b/FRAuth/FRAuthTests/FRAuthSwiftTests/E2ETests/Callback-Live/AA_12_ReCaptchaEnterpriseCallbackTest.swift @@ -0,0 +1,448 @@ +// +// AA_09_PingOneProtectInitializeCallbackTest.swift +// FRAuthTests +// +// Copyright (c) 2024 ForgeRock. All rights reserved. +// +// This software may be modified and distributed under the terms +// of the MIT license. See the LICENSE file for details. +// + +import XCTest +@testable import FRAuth +@testable import RecaptchaEnterprise +import FRCaptchaEnterprise + +@available(iOS 13, *) +class AA_12_ReCaptchaEnterpriseCallbackTest: CallbackBaseTest { + + static var USERNAME: String = "sdkuser" + static var SITE_KEY: String = "6Lc0NUIqAAAAALRSrhXb5CWrZPzWkezBFB_0mnqS" // this is configured in the test joruney + + let options = FROptions(url: "https://openam-recaptcha.forgeblocks.com/am", + realm: "alpha", + enableCookie: true, + cookieName: "b431aeda2ba0e98", + timeout: "180", + authServiceName: "TEST-e2e-recaptcha-enterprise", + oauthThreshold: "60", + oauthClientId: "iosclient", + oauthRedirectUri: "http://localhost:8081", + oauthScope: "openid profile email address", + keychainAccessGroup: "com.bitbar.*" + ) + + override func setUp() { + do { + try FRAuth.start(options: options) + } + catch { + XCTFail("Fail to start the the SDK with custom config.") + } + } + + override func tearDown() { + FRSession.currentSession?.logout() + super.tearDown() + } + + func test_01_recaptcha_enterprise_success() async throws { + var currentNode: Node + + do { + try currentNode = await startTest(nodeConfiguration: "success") + } catch AuthError.invalidCallbackResponse { + XCTFail("Expected a ReCaptchaEnterprise node, but got nothing!") + return + } catch { + XCTFail("Unexpected error occured!") + return + } + + var tokenResult = "" // Used to verify that AM uses the same token acquired by the SDK + // We expect ReCaptchaEnterpriseCallback callback here... + for callback in currentNode.callbacks { + if callback is ReCaptchaEnterpriseCallback, let reCaptchaEnterpriseCallback = callback as? ReCaptchaEnterpriseCallback { + XCTAssertEqual(reCaptchaEnterpriseCallback.recaptchaSiteKey, AA_12_ReCaptchaEnterpriseCallbackTest.SITE_KEY) + do { + try await reCaptchaEnterpriseCallback.execute() + XCTAssertNotNil(reCaptchaEnterpriseCallback.inputValues[reCaptchaEnterpriseCallback.tokenKey] as? String) + XCTAssertNotNil(reCaptchaEnterpriseCallback.tokenResult) + tokenResult = reCaptchaEnterpriseCallback.tokenResult // We are going to configure later that the same token is used in AM + } + catch let error as RecaptchaError { + XCTFail("reCaptchaEnterpriseCallback.execute() failed: \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + var ex = self.expectation(description: "Submit ReCaptchaEnterpriseCallback callback and continue...") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node! + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + // Confirm that reCAPTCHA Enterprise node execution is successulf. + // Note: Upon success the test tree returns CaptchaEnterpriseNode.ASSESSMENT_RESULT in a TextOutput callback... + for callback in currentNode.callbacks { + if callback is TextOutputCallback, let textOutputCallback = callback as? TextOutputCallback { + let assessmentData = Data(textOutputCallback.message.utf8) + + do { + if let assessmentDictionary = try JSONSerialization.jsonObject(with: assessmentData, options: []) as? [String: Any] { + // Assert a few things in the assessment data: + let event = assessmentDictionary["event"] as! [String: Any] + let tokenProperties = assessmentDictionary["tokenProperties"] as! [String: Any] + let riskAnalysis = assessmentDictionary["riskAnalysis"] as! [String: Any] + + let userAgent = event["userAgent"] as! String // e.g: "FRTestHost/1 CFNetwork/1406.0.4 Darwin/23.6.0" + let siteKey = event["siteKey"] as! String // Should be the one configured in the ReCaptcha node + let userIpAddress = event["userIpAddress"] as! String + let token = event["token"] as! String + let action = tokenProperties["action"] as! String // should be "login" by default + let valid = tokenProperties["valid"] as! Bool // should be valid + let iosBundleId = tokenProperties["iosBundleId"] as! String // should be "com.forgerock.FRTestHost" + let score = riskAnalysis["score"] as! Double + + XCTAssertEqual(siteKey, AA_12_ReCaptchaEnterpriseCallbackTest.SITE_KEY) + XCTAssert(userAgent.contains("Darwin")) + XCTAssert(!userIpAddress.isEmpty) + XCTAssertEqual(token, tokenResult) + XCTAssertEqual(action, "login") + XCTAssertTrue(valid) + XCTAssertEqual(iosBundleId, "com.forgerock.FRTestHost") + XCTAssertGreaterThanOrEqual(score, 0.0) + XCTAssertLessThanOrEqual(score, 1.0) + } + } catch let error as NSError { + XCTFail("Error parsing the assessment data \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit the TextOutput callback and continue with the flow") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + /// At the end the user should be logged in + XCTAssertNotNil(FRUser.currentUser) + } + + func test_02_recaptcha_enterprise_success_custom() async throws { + var currentNode: Node + + do { + try currentNode = await startTest(nodeConfiguration: "success") + } catch AuthError.invalidCallbackResponse { + XCTFail("Expected a ReCaptchaEnterprise node, but got nothing!") + return + } catch { + XCTFail("Unexpected error occured!") + return + } + + // We expect ReCaptchaEnterpriseCallback callback here... + for callback in currentNode.callbacks { + if callback is ReCaptchaEnterpriseCallback, let reCaptchaEnterpriseCallback = callback as? ReCaptchaEnterpriseCallback { + do { + // Set additional payload and custom action + reCaptchaEnterpriseCallback.setPayload(["firewallPolicyEvaluation": false, + "express": false, + "transaction_data": [ + "transaction_id": "custom-payload-1234567890", + "payment_method": "credit-card", + "card_bin": "1111", + "card_last_four": "1234", + "currency_code": "CAD" + ], + ]) + try await reCaptchaEnterpriseCallback.execute(action: "custom_action") + } + catch let error as RecaptchaError { + XCTFail("reCaptchaEnterpriseCallback.execute() failed: \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + var ex = self.expectation(description: "Submit ReCaptchaEnterpriseCallback callback and continue...") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node! + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + // Confirm that reCAPTCHA Enterprise node execution is successulf. + // Note: Upon success the test tree returns CaptchaEnterpriseNode.ASSESSMENT_RESULT in a TextOutput callback... + for callback in currentNode.callbacks { + if callback is TextOutputCallback, let textOutputCallback = callback as? TextOutputCallback { + let assessmentData = Data(textOutputCallback.message.utf8) + + do { + if let assessmentDictionary = try JSONSerialization.jsonObject(with: assessmentData, options: []) as? [String: Any] { + // Assert that the custom payload and action has been taken into account...: + + let event = assessmentDictionary["event"] as! [String: Any] + let tokenProperties = assessmentDictionary["tokenProperties"] as! [String: Any] + let transactionData = event["transactionData"] as! [String: Any] + + let firewallPolicyEvaluation = event["firewallPolicyEvaluation"] as! Bool + let express = event["express"] as! Bool + let action = tokenProperties["action"] as! String // should be "custom_action" by default + let valid = tokenProperties["valid"] as! Bool // should be valid + + let transactionId = transactionData["transactionId"] as! String // should be "custom-payload-1234567890" + let paymentMthod = transactionData["paymentMethod"] as! String // should be "credit-card", + let cardBin = transactionData["cardBin"] as! String // should be "1111", + let cardLastFour = transactionData["cardLastFour"] as! String // should be "1234", + let currencyCode = transactionData["currencyCode"] as! String // should be "CAD", + + XCTAssertFalse(firewallPolicyEvaluation) // This is to prove that custom payload has been applied + XCTAssertFalse(express) + XCTAssertEqual(action, "custom_action") + XCTAssertTrue(valid) + + // These come from the custom payload: + XCTAssertEqual(transactionId, "custom-payload-1234567890") + XCTAssertEqual(paymentMthod, "credit-card") + XCTAssertEqual(cardBin, "1111") + XCTAssertEqual(cardLastFour, "1234") + XCTAssertEqual(currencyCode, "CAD") + } + } catch let error as NSError { + XCTFail("Error parsing the assessment data \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit the TextOutput callback and continue with the flow") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + /// At the end the user should be logged in + XCTAssertNotNil(FRUser.currentUser) + } + + func test_03_recaptcha_enterprise_fail_score() async throws { + var currentNode: Node + + do { + try currentNode = await startTest(nodeConfiguration: "score_failure") + } catch AuthError.invalidCallbackResponse { + XCTFail("Expected a ReCaptchaEnterprise node, but got nothing!") + return + } catch { + XCTFail("Unexpected error occured!") + return + } + + // We expect ReCaptchaEnterpriseCallback callback here... + for callback in currentNode.callbacks { + if callback is ReCaptchaEnterpriseCallback, let reCaptchaEnterpriseCallback = callback as? ReCaptchaEnterpriseCallback { + XCTAssertNotNil(reCaptchaEnterpriseCallback.recaptchaSiteKey) + do { + try await reCaptchaEnterpriseCallback.execute() + } + catch let error as RecaptchaError { + XCTFail("reCaptchaEnterpriseCallback.execute() failed: \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + var ex = self.expectation(description: "Submit ReCaptchaEnterpriseCallback callback and continue...") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node! + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + // Confirm that reCAPTCHA Enterprise node execution fails (since the "Score threshold" is set to 1.0) + // Note: Upon failure the test tree returns CaptchaEnterpriseNode.FAILURE in a TextOutput callback... + for callback in currentNode.callbacks { + if callback is TextOutputCallback, let textOutputCallback = callback as? TextOutputCallback { + XCTAssertEqual(textOutputCallback.message, "\"VALIDATION_ERROR:CAPTCHA validation failed\"") + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit the TextOutput callback and continue with the flow") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + /// At the end the user should be logged in + XCTAssertNotNil(FRUser.currentUser) + } + + func test_04_recaptcha_enterprise_custom_client_error() async throws { + var currentNode: Node + + do { + try currentNode = await startTest(nodeConfiguration: "custom_client_error") + } catch AuthError.invalidCallbackResponse { + XCTFail("Expected a ReCaptchaEnterprise node, but got nothing!") + return + } catch { + XCTFail("Unexpected error occured!") + return + } + + // We expect ReCaptchaEnterpriseCallback callback here... + for callback in currentNode.callbacks { + if callback is ReCaptchaEnterpriseCallback, let reCaptchaEnterpriseCallback = callback as? ReCaptchaEnterpriseCallback { + do { + reCaptchaEnterpriseCallback.setClientError("CUSTOM_CLIENT_ERROR") + try await reCaptchaEnterpriseCallback.execute() + } + catch let error as RecaptchaError { + XCTFail("reCaptchaEnterpriseCallback.execute() failed: \(error)") + } + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + var ex = self.expectation(description: "Submit ReCaptchaEnterpriseCallback callback and continue...") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node! + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + // Confirm that reCAPTCHA Enterprise node execution fails (since we have set client error) + // Note: Upon failure the test tree returns CaptchaEnterpriseNode.FAILURE in a TextOutput callback... + for callback in currentNode.callbacks { + if callback is TextOutputCallback, let textOutputCallback = callback as? TextOutputCallback { + XCTAssertEqual(textOutputCallback.message, "\"CLIENT_ERROR:CUSTOM_CLIENT_ERROR\"") + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit the TextOutput callback and continue with the flow") + currentNode.next { (token: AccessToken?, node, error) in + XCTAssertNil(node) + XCTAssertNil(error) + XCTAssertNotNil(token) + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + /// At the end the user should be logged in + XCTAssertNotNil(FRUser.currentUser) + } + + /// Common steps for all test cases + func startTest(nodeConfiguration: String) async throws -> Node { + var currentNode: Node? + + var ex = self.expectation(description: "Choose test case") + FRSession.authenticate(authIndexValue: options.authServiceName) { (token: Token?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + // Provide input value for the username collector callback + for callback in currentNode!.callbacks { + if callback is NameCallback, let nameCallback = callback as? NameCallback { + nameCallback.setValue(AA_12_ReCaptchaEnterpriseCallbackTest.USERNAME) + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit username to the Username Collector Node") + currentNode?.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + /// -------------------------------- + guard let node = currentNode else { + XCTFail("Failed to get Node") + throw AuthError.invalidCallbackResponse("Expected ChoiceCollector node, but got nothing...") + } + + // Provide input value for the ChoiceCollector callback + for callback in node.callbacks { + if callback is ChoiceCallback, let choiceCallback = callback as? ChoiceCallback { + let choiceIndex = choiceCallback.choices.firstIndex(of: nodeConfiguration) + choiceCallback.setValue(choiceIndex) + } + else { + XCTFail("Received unexpected callback \(callback)") + } + } + + ex = self.expectation(description: "Submit value to the ChoiceCollector Node") + currentNode?.next { (token: AccessToken?, node, error) in + XCTAssertNil(token) + XCTAssertNil(error) + XCTAssertNotNil(node) + currentNode = node + ex.fulfill() + } + await fulfillment(of: [ex], timeout: 60.0) + + guard currentNode != nil else { + XCTFail("Failed to get Node from the second request") + throw AuthError.invalidCallbackResponse("Expected at least one more node, but got nothing...") + } + + return currentNode! + } +} diff --git a/FRCaptchaEnterprise/FRCaptchaEnterprise.xcodeproj/project.pbxproj b/FRCaptchaEnterprise/FRCaptchaEnterprise.xcodeproj/project.pbxproj index d07fd987..401a634d 100644 --- a/FRCaptchaEnterprise/FRCaptchaEnterprise.xcodeproj/project.pbxproj +++ b/FRCaptchaEnterprise/FRCaptchaEnterprise.xcodeproj/project.pbxproj @@ -8,7 +8,7 @@ /* Begin PBXBuildFile section */ 1BA9F00B2C755B390033651D /* FRCaptchaEnterprise.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1BA9F0002C755B390033651D /* FRCaptchaEnterprise.framework */; }; - 1BA9F0102C755B390033651D /* FRCaptchaEnterpriseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA9F00F2C755B390033651D /* FRCaptchaEnterpriseTests.swift */; }; + 1BA9F0102C755B390033651D /* ReCaptchaEnterpriseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BA9F00F2C755B390033651D /* ReCaptchaEnterpriseTests.swift */; }; 1BA9F0112C755B390033651D /* FRCaptchaEnterprise.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BA9F0032C755B390033651D /* FRCaptchaEnterprise.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1BA9F02F2C755EB90033651D /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 1BA9F02E2C755EAD0033651D /* PrivacyInfo.xcprivacy */; }; 1BE1B7AF2C767C5800B12207 /* ReCaptchaEnterpriseCallback.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1BE1B7AE2C767C5800B12207 /* ReCaptchaEnterpriseCallback.swift */; }; @@ -121,7 +121,7 @@ 1BA9F0002C755B390033651D /* FRCaptchaEnterprise.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = FRCaptchaEnterprise.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1BA9F0032C755B390033651D /* FRCaptchaEnterprise.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FRCaptchaEnterprise.h; sourceTree = ""; }; 1BA9F00A2C755B390033651D /* FRCaptchaEnterpriseTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FRCaptchaEnterpriseTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 1BA9F00F2C755B390033651D /* FRCaptchaEnterpriseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FRCaptchaEnterpriseTests.swift; sourceTree = ""; }; + 1BA9F00F2C755B390033651D /* ReCaptchaEnterpriseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReCaptchaEnterpriseTests.swift; sourceTree = ""; }; 1BA9F02E2C755EAD0033651D /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 1BE1B7AE2C767C5800B12207 /* ReCaptchaEnterpriseCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReCaptchaEnterpriseCallback.swift; sourceTree = ""; }; 1BE1B7B12C767EB400B12207 /* FRAuth.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = FRAuth.xcodeproj; path = ../FRAuth/FRAuth.xcodeproj; sourceTree = ""; }; @@ -246,7 +246,7 @@ isa = PBXGroup; children = ( 1BE1B8212C76B9BE00B12207 /* SharedTestFiles */, - 1BA9F00F2C755B390033651D /* FRCaptchaEnterpriseTests.swift */, + 1BA9F00F2C755B390033651D /* ReCaptchaEnterpriseTests.swift */, ); path = FRCaptchaEnterpriseTests; sourceTree = ""; @@ -674,7 +674,7 @@ 1BE1B85D2C76B9BF00B12207 /* FRTestNetworkStubProtocol.swift in Sources */, 1BE1B85F2C76B9BF00B12207 /* FRTestUtils.swift in Sources */, 1BE1B8222C76B9BE00B12207 /* FRAuthBaseTest.swift in Sources */, - 1BA9F0102C755B390033651D /* FRCaptchaEnterpriseTests.swift in Sources */, + 1BA9F0102C755B390033651D /* ReCaptchaEnterpriseTests.swift in Sources */, 1BE1B85E2C76B9BF00B12207 /* FRTestStubResponseParser.swift in Sources */, 1BE1B82C2C76B9BE00B12207 /* CustomCallback.swift in Sources */, ); @@ -964,7 +964,7 @@ repositoryURL = "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 18.5.1; + minimumVersion = 18.6.0; }; }; /* End XCRemoteSwiftPackageReference section */ diff --git a/FRCaptchaEnterprise/FRCaptchaEnterprise/Sources/Callbacks/ReCaptchaEnterpriseCallback.swift b/FRCaptchaEnterprise/FRCaptchaEnterprise/Sources/Callbacks/ReCaptchaEnterpriseCallback.swift index bd4ec979..6bd2729f 100644 --- a/FRCaptchaEnterprise/FRCaptchaEnterprise/Sources/Callbacks/ReCaptchaEnterpriseCallback.swift +++ b/FRCaptchaEnterprise/FRCaptchaEnterprise/Sources/Callbacks/ReCaptchaEnterpriseCallback.swift @@ -23,6 +23,8 @@ public class ReCaptchaEnterpriseCallback: MultipleValuesCallback { public private(set) var recaptchaSiteKey: String = String() /// Token input key in callback response public private(set) var tokenKey: String = String() + /// Token result + public private(set) var tokenResult: String = String() /// Action input key in callback response public private(set) var actionKey: String = String() /// Client Error input key in callback response @@ -99,12 +101,14 @@ public class ReCaptchaEnterpriseCallback: MultipleValuesCallback { public func execute(action: String = "login", timeoutInMillis: Double = 15000, recaptchaProvider: RecaptchaClientProvider = DefaultRecaptchaClientProvider()) async throws { do { - let recaptchaClient: RecaptchaClient? = try await recaptchaProvider.getClient(withSiteKey: recaptchaSiteKey, withTimeout: timeoutInMillis) - let action = RecaptchaAction(customAction: action) - let token: String? = try await recaptchaProvider.execute(recaptchaClient: recaptchaClient, action: action, timeout: timeoutInMillis) + let recaptchaClient: RecaptchaClient? = try await recaptchaProvider.fetchClient(withSiteKey: recaptchaSiteKey) + let recaptchaAction = RecaptchaAction(customAction: action) + let token: String? = try await recaptchaProvider.execute(recaptchaClient: recaptchaClient, action: recaptchaAction, timeout: timeoutInMillis) guard let result = token else { throw NSError(domain: CaptchaConstant.domain, code: 1, userInfo: [NSLocalizedDescriptionKey: CaptchaConstant.invalidToken]) } + self.setAction(action) + self.tokenResult = result self.setToken(result) } catch { @@ -137,7 +141,7 @@ public class ReCaptchaEnterpriseCallback: MultipleValuesCallback { /// Sets `action` value for the ReCAPTCHA in callback response /// - Parameter value: String value of `action` - public func setAction(_ value: String) { + internal func setAction(_ value: String) { self.inputValues[self.actionKey] = value } @@ -147,9 +151,9 @@ public class ReCaptchaEnterpriseCallback: MultipleValuesCallback { self.inputValues[self.clientErrorKey] = value } - /// Sets `additionalJson` value for the ReCAPTCHA in callback response + /// Sets additional payload value for the ReCAPTCHA in callback response /// - Parameter value: Dictionary value of `additionalJson` - public func setAdditionalJson(_ value: [String: Any]? = nil) { + public func setPayload(_ value: [String: Any]? = nil) { if let payload = value, !payload.isEmpty { self.inputValues[self.payloadKey] = JSONStringify(value: payload) } @@ -160,10 +164,9 @@ public class ReCaptchaEnterpriseCallback: MultipleValuesCallback { // MARK: - RecaptchaClientProvider @available(iOS 13, *) public protocol RecaptchaClientProvider { - /// Get RecaptchaClient with given siteKey and timeout + /// Fetch RecaptchaClient with given siteKey /// - Parameter siteKey: String value of siteKey - /// - Parameter timeout: Double value of timeout - func getClient(withSiteKey siteKey: String, withTimeout timeout: Double) async throws -> RecaptchaClient? + func fetchClient(withSiteKey siteKey: String) async throws -> RecaptchaClient? /// Execute RecaptchaClient with given action and timeout /// - Parameter recaptchaClient: RecaptchaClient instance @@ -175,11 +178,10 @@ public protocol RecaptchaClientProvider { @available(iOS 13, *) public struct DefaultRecaptchaClientProvider: RecaptchaClientProvider { public init(){} - /// Get RecaptchaClient with given siteKey and timeout + /// Fetch RecaptchaClient with given siteKey and timeout /// - Parameter siteKey: String value of siteKey - /// - Parameter timeout: Double value of timeout - public func getClient(withSiteKey siteKey: String, withTimeout timeout: Double) async throws -> RecaptchaClient? { - return try await Recaptcha.getClient(withSiteKey: siteKey, withTimeout: timeout) + public func fetchClient(withSiteKey siteKey: String) async throws -> RecaptchaClient? { + return try await Recaptcha.fetchClient(withSiteKey: siteKey) } /// Execute RecaptchaClient with given action and timeout diff --git a/FRCaptchaEnterprise/FRCaptchaEnterpriseTests/FRCaptchaEnterpriseTests.swift b/FRCaptchaEnterprise/FRCaptchaEnterpriseTests/ReCaptchaEnterpriseTests.swift similarity index 97% rename from FRCaptchaEnterprise/FRCaptchaEnterpriseTests/FRCaptchaEnterpriseTests.swift rename to FRCaptchaEnterprise/FRCaptchaEnterpriseTests/ReCaptchaEnterpriseTests.swift index d02c8742..bc3f0026 100644 --- a/FRCaptchaEnterprise/FRCaptchaEnterpriseTests/FRCaptchaEnterpriseTests.swift +++ b/FRCaptchaEnterprise/FRCaptchaEnterpriseTests/ReCaptchaEnterpriseTests.swift @@ -14,7 +14,7 @@ import XCTest @testable import FRAuth @available(iOS 13, *) -final class FRCaptchaEnterpriseTests: FRAuthBaseTest { +final class ReCaptchaEnterpriseTests: FRAuthBaseTest { var mockProvider: MockRecaptchaClientProvider! @@ -183,7 +183,6 @@ final class FRCaptchaEnterpriseTests: FRAuthBaseTest { // Verify captured parameters XCTAssertEqual(mockProvider.capturedSiteKey, "siteKey") - XCTAssertEqual(mockProvider.capturedClientTimeout, 15000) XCTAssertEqual(mockProvider.capturedAction?.action, nil) } @@ -268,7 +267,7 @@ final class FRCaptchaEnterpriseTests: FRAuthBaseTest { // Test case 1: Set valid JSON payload let validJson: [String: Any] = ["key": "value"] - callback.setAdditionalJson(validJson) + callback.setPayload(validJson) XCTAssertTrue((callback.inputValues[callback.payloadKey] as! String).contains("value")) } @@ -280,7 +279,6 @@ final class FRCaptchaEnterpriseTests: FRAuthBaseTest { let callbackResponse = self.parseStringToDictionary(jsonStr) let callback = try ReCaptchaEnterpriseCallback(json: callbackResponse) - // Test case 1: Set valid JSON payload let action = "login_test" callback.setAction(action) @@ -288,7 +286,6 @@ final class FRCaptchaEnterpriseTests: FRAuthBaseTest { XCTAssertEqual(callback.inputValues[callback.actionKey] as? String, "login_test") XCTAssertEqual(callback.inputValues[callback.clientErrorKey] as? String, "customClientError") - } } @@ -307,10 +304,9 @@ class MockRecaptchaClientProvider: RecaptchaClientProvider { var capturedClientTimeout: Double? var capturedAction: RecaptchaAction? - func getClient(withSiteKey siteKey: String, withTimeout timeout: Double) async throws -> RecaptchaClient? { + func fetchClient(withSiteKey siteKey: String) async throws -> RecaptchaClient? { capturedSiteKey = siteKey - capturedClientTimeout = timeout if let error = shouldThrowErrorIntialization { throw error @@ -320,6 +316,7 @@ class MockRecaptchaClientProvider: RecaptchaClientProvider { func execute(recaptchaClient: RecaptchaClient?, action: RecaptchaAction, timeout: Double) async throws -> String? { capturedAction = action + capturedClientTimeout = timeout if let shouldthrowError = shouldthrowError { throw shouldthrowError } diff --git a/FRTestHost/FRTestHost/FRAuthConfigPKHash.plist b/FRTestHost/FRTestHost/FRAuthConfigPKHash.plist index 4543b96e..c5216157 100644 --- a/FRTestHost/FRTestHost/FRAuthConfigPKHash.plist +++ b/FRTestHost/FRTestHost/FRAuthConfigPKHash.plist @@ -26,7 +26,7 @@ signUp forgerock_ssl_pinning_public_key_hashes - IFG+z/oQKXfpUYOHgWHy5axgkT9B01XSxwb2AHDyN34= + C5+lpZ7tcVwmwQIMcRtPbsQtWLABXhQzejna0wHFr8M= 7wkd0YUvyMiF/H74I7FLGYVcIsb8eFDhjA8Aqq0S2+U= QqdOOH5lPT79vRggY1h13mgwQ1CJTaAi8oLkuKZnk74= diff --git a/Package.swift b/Package.swift index 40ab5dcc..3d49be0e 100644 --- a/Package.swift +++ b/Package.swift @@ -23,7 +23,7 @@ let package = Package ( .package(name: "GoogleSignIn", url: "https://github.com/google/GoogleSignIn-iOS.git", .upToNextMinor(from: "7.1.0")), .package(name: "JOSESwift", url: "https://github.com/airsidemobile/JOSESwift.git", .upToNextMinor(from: "2.4.0")), .package(name: "PingOneSignals", url: "https://github.com/pingidentity/pingone-signals-sdk-ios.git", .upToNextMinor(from: "5.2.3")), - .package(name: "RecaptchaEnterprise", url: "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk.git", .upToNextMinor(from: "18.5.1")) + .package(name: "RecaptchaEnterprise", url: "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk.git", .upToNextMinor(from: "18.6.0")) ], targets: [ .target(name: "cFRCore", dependencies: [], path: "FRCore/FRCore/SharedC/Sources"), diff --git a/SampleApps/FRExample.xcworkspace/xcshareddata/swiftpm/Package.resolved b/SampleApps/FRExample.xcworkspace/xcshareddata/swiftpm/Package.resolved index c19d7307..bdd564ab 100644 --- a/SampleApps/FRExample.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/SampleApps/FRExample.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -78,8 +78,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GoogleCloudPlatform/recaptcha-enterprise-mobile-sdk", "state" : { - "revision" : "742de2369422c0e5395205b9215b6b7228dc206a", - "version" : "18.5.1" + "revision" : "e1890d87d0aa0780baa38f46bf09db3630bd0beb", + "version" : "18.6.0" } } ],