diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index b8abba0a933..c484d7404b0 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -1293,6 +1293,8 @@ extension Auth: AuthInterop { return false } + let recaptchaVerifier: AuthRecaptchaVerifier + #if os(iOS) && !targetEnvironment(macCatalyst) /// Initializes reCAPTCHA using the settings configured for the project or tenant. @@ -1322,8 +1324,10 @@ extension Auth: AuthInterop { open func initializeRecaptchaConfig() async throws { // Trigger recaptcha verification flow to initialize the recaptcha client and // config. Recaptcha token will be returned. - let verifier = AuthRecaptchaVerifier.shared(auth: self) - _ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction) + _ = try await recaptchaVerifier.verify( + forceRefresh: true, + action: AuthRecaptchaAction.defaultAction + ) } #endif @@ -1623,7 +1627,8 @@ extension Auth: AuthInterop { init(app: FirebaseApp, keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal(), backend: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()), - authDispatcher: AuthDispatcher = .init()) { + authDispatcher: AuthDispatcher = .init(), + recaptchaVerifier: AuthRecaptchaVerifier = .init()) { self.app = app mainBundleUrlTypes = Bundle.main .object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]] @@ -1649,6 +1654,7 @@ extension Auth: AuthInterop { appCheck: appCheck) self.backend = backend self.authDispatcher = authDispatcher + self.recaptchaVerifier = recaptchaVerifier let keychainServiceName = Auth.keychainServiceName(for: app) keychainServices = AuthKeychainServices(service: keychainServiceName, @@ -1660,6 +1666,7 @@ extension Auth: AuthInterop { super.init() requestConfiguration.auth = self + self.recaptchaVerifier.auth = self protectedDataInitialization() } @@ -2306,7 +2313,6 @@ extension Auth: AuthInterop { func injectRecaptcha(request: T, action: AuthRecaptchaAction) async throws -> T .Response { - let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self) if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) != .off { try await recaptchaVerifier.injectRecaptchaFields(request: request, provider: AuthRecaptchaProvider.password, diff --git a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift index 3e72368126c..b412d616cf1 100644 --- a/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift +++ b/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift @@ -202,10 +202,9 @@ import Foundation throw AuthErrorUtils.notificationNotForwardedError() } - let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth) - try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true) + try await auth.recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true) - switch recaptchaVerifier.enablementStatus(forProvider: .phone) { + switch auth.recaptchaVerifier.enablementStatus(forProvider: .phone) { case .off: return try await verifyClAndSendVerificationCode( toPhoneNumber: phoneNumber, @@ -218,31 +217,28 @@ import Foundation toPhoneNumber: phoneNumber, retryOnInvalidAppCredential: true, multiFactorSession: multiFactorSession, - uiDelegate: uiDelegate, - recaptchaVerifier: recaptchaVerifier + uiDelegate: uiDelegate ) case .enforce: return try await verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: phoneNumber, retryOnInvalidAppCredential: false, multiFactorSession: multiFactorSession, - uiDelegate: uiDelegate, - recaptchaVerifier: recaptchaVerifier + uiDelegate: uiDelegate ) } } func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, - uiDelegate: AuthUIDelegate?, - recaptchaVerifier: AuthRecaptchaVerifier) async throws + uiDelegate: AuthUIDelegate?) async throws -> String? { let request = SendVerificationCodeRequest(phoneNumber: phoneNumber, codeIdentity: CodeIdentity.empty, requestConfiguration: auth .requestConfiguration) do { - try await recaptchaVerifier.injectRecaptchaFields( + try await auth.recaptchaVerifier.injectRecaptchaFields( request: request, provider: .phone, action: .sendVerificationCode @@ -304,8 +300,7 @@ import Foundation private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String, retryOnInvalidAppCredential: Bool, multiFactorSession session: MultiFactorSession?, - uiDelegate: AuthUIDelegate?, - recaptchaVerifier: AuthRecaptchaVerifier) async throws + uiDelegate: AuthUIDelegate?) async throws -> String? { if let settings = auth.settings, settings.isAppVerificationDisabledForTesting { @@ -321,8 +316,7 @@ import Foundation return try await verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: phoneNumber, retryOnInvalidAppCredential: retryOnInvalidAppCredential, - uiDelegate: uiDelegate, - recaptchaVerifier: recaptchaVerifier + uiDelegate: uiDelegate ) } let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber, @@ -332,7 +326,7 @@ import Foundation let request = StartMFAEnrollmentRequest(idToken: idToken, enrollmentInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - try await recaptchaVerifier.injectRecaptchaFields( + try await auth.recaptchaVerifier.injectRecaptchaFields( request: request, provider: .phone, action: .mfaSmsEnrollment @@ -344,7 +338,7 @@ import Foundation MFAEnrollmentID: session.multiFactorInfo?.uid, signInInfo: startMFARequestInfo, requestConfiguration: auth.requestConfiguration) - try await recaptchaVerifier.injectRecaptchaFields( + try await auth.recaptchaVerifier.injectRecaptchaFields( request: request, provider: .phone, action: .mfaSmsSignIn @@ -627,7 +621,6 @@ import Foundation private let auth: Auth private let callbackScheme: String private let usingClientIDScheme: Bool - private var recaptchaVerifier: AuthRecaptchaVerifier? init(auth: Auth) { self.auth = auth @@ -648,7 +641,6 @@ import Foundation return } callbackScheme = "" - recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth) } private let kAuthTypeVerifyApp = "verifyApp" diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index bb3373938e8..6fe08bdf07d 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -12,267 +12,247 @@ // See the License for the specific language governing permissions and // limitations under the License. -#if os(iOS) +import Foundation - import Foundation +#if SWIFT_PACKAGE + import FirebaseAuthInternal +#endif - #if SWIFT_PACKAGE - import FirebaseAuthInternal - #endif - import RecaptchaInterop +import RecaptchaInterop - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - class AuthRecaptchaConfig { - var siteKey: String? - let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] +@available(iOS 13, *) +class AuthRecaptchaConfig { + var siteKey: String? + let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] - init(siteKey: String? = nil, - enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) { - self.siteKey = siteKey - self.enablementStatus = enablementStatus - } + init(siteKey: String? = nil, + enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) { + self.siteKey = siteKey + self.enablementStatus = enablementStatus } +} - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - enum AuthRecaptchaEnablementStatus: String, CaseIterable { - case enforce = "ENFORCE" - case audit = "AUDIT" - case off = "OFF" +@available(iOS 13, *) +enum AuthRecaptchaEnablementStatus: String, CaseIterable { + case enforce = "ENFORCE" + case audit = "AUDIT" + case off = "OFF" - // Convenience property for mapping values - var stringValue: String { rawValue } - } + // Convenience property for mapping values + var stringValue: String { rawValue } +} - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - enum AuthRecaptchaProvider: String, CaseIterable { - case password = "EMAIL_PASSWORD_PROVIDER" - case phone = "PHONE_PROVIDER" +@available(iOS 13, *) +enum AuthRecaptchaProvider: String, CaseIterable { + case password = "EMAIL_PASSWORD_PROVIDER" + case phone = "PHONE_PROVIDER" - // Convenience property for mapping values - var stringValue: String { rawValue } - } + // Convenience property for mapping values + var stringValue: String { rawValue } +} - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - enum AuthRecaptchaAction: String { - case defaultAction - case signInWithPassword - case getOobCode - case signUpPassword - case sendVerificationCode - case mfaSmsSignIn - case mfaSmsEnrollment +@available(iOS 13, *) +enum AuthRecaptchaAction: String { + case defaultAction + case signInWithPassword + case getOobCode + case signUpPassword + case sendVerificationCode + case mfaSmsSignIn + case mfaSmsEnrollment - // Convenience property for mapping values - var stringValue: String { rawValue } - } + // Convenience property for mapping values + var stringValue: String { rawValue } +} - @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) - class AuthRecaptchaVerifier { - private(set) weak var auth: Auth? - private(set) var agentConfig: AuthRecaptchaConfig? - private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:] - private(set) var recaptchaClient: RCARecaptchaClientProtocol? - private static var _shared = AuthRecaptchaVerifier() - private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" - init() {} +@available(iOS 13, *) +class AuthRecaptchaVerifier { + weak var auth: Auth? + private var agentConfig: AuthRecaptchaConfig? + private var tenantConfigs: [String: AuthRecaptchaConfig] = [:] + private var recaptchaClient: RCARecaptchaClientProtocol? + private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE" - class func shared(auth: Auth?) -> AuthRecaptchaVerifier { - if _shared.auth != auth { - _shared.agentConfig = nil - _shared.tenantConfigs = [:] - _shared.auth = auth - } - return _shared + func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String { + try await retrieveRecaptchaConfig(forceRefresh: forceRefresh) + guard let siteKey = siteKey() else { + throw AuthErrorUtils.recaptchaSiteKeyMissing() } + let actionString = action.stringValue + #if !(COCOAPODS || SWIFT_PACKAGE) + // No recaptcha on internal build system. + return actionString + #else + let (token, error, linked, actionCreated) = await recaptchaToken( + siteKey: siteKey, + actionString: actionString, + fakeToken: "NO_RECAPTCHA" + ) + + guard linked else { + throw AuthErrorUtils.recaptchaSDKNotLinkedError() + } + guard actionCreated else { + throw AuthErrorUtils.recaptchaActionCreationFailed() + } + if let error { + throw error + } + if token == "NO_RECAPTCHA" { + AuthLog.logInfo(code: "I-AUT000031", + message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.") + } else { + AuthLog.logInfo( + code: "I-AUT000030", + message: "reCAPTCHA token retrieval succeeded." + ) + } + return token + #endif // !(COCOAPODS || SWIFT_PACKAGE) + } - /// This function is only for testing. - class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) { - _shared = instance - _ = shared(auth: auth) + func enablementStatus(forProvider provider: AuthRecaptchaProvider) + -> AuthRecaptchaEnablementStatus { + if let tenantID = auth?.tenantID, + let tenantConfig = tenantConfigs[tenantID], + let status = tenantConfig.enablementStatus[provider] { + return status + } else if let agentConfig = agentConfig, + let status = agentConfig.enablementStatus[provider] { + return status + } else { + return AuthRecaptchaEnablementStatus.off } + } - func siteKey() -> String? { + func retrieveRecaptchaConfig(forceRefresh: Bool) async throws { + if !forceRefresh { if let tenantID = auth?.tenantID { - if let config = tenantConfigs[tenantID] { - return config.siteKey + if tenantConfigs[tenantID] != nil { + return } - return nil + } else if agentConfig != nil { + return } - return agentConfig?.siteKey } - func enablementStatus(forProvider provider: AuthRecaptchaProvider) - -> AuthRecaptchaEnablementStatus { - if let tenantID = auth?.tenantID, - let tenantConfig = tenantConfigs[tenantID], - let status = tenantConfig.enablementStatus[provider] { - return status - } else if let agentConfig = agentConfig, - let status = agentConfig.enablementStatus[provider] { - return status - } else { - return AuthRecaptchaEnablementStatus.off - } + guard let auth = auth else { + throw AuthErrorUtils.error(code: .recaptchaNotEnabled, + message: "No requestConfiguration for Auth instance") } + let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration) + let response = try await auth.backend.call(with: request) + AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.") + try await parseRecaptchaConfigFromResponse(response: response) + } - func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String { - try await retrieveRecaptchaConfig(forceRefresh: forceRefresh) - guard let siteKey = siteKey() else { - throw AuthErrorUtils.recaptchaSiteKeyMissing() - } - let actionString = action.stringValue - #if !(COCOAPODS || SWIFT_PACKAGE) - // No recaptcha on internal build system. - return actionString - #else - - let (token, error, linked, actionCreated) = await recaptchaToken( - siteKey: siteKey, - actionString: actionString, - fakeToken: "NO_RECAPTCHA" - ) + func injectRecaptchaFields(request: any AuthRPCRequest, + provider: AuthRecaptchaProvider, + action: AuthRecaptchaAction) async throws { + try await retrieveRecaptchaConfig(forceRefresh: false) + if enablementStatus(forProvider: provider) != .off { + let token = try await verify(forceRefresh: false, action: action) + request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion) + } else { + request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion) + } + } - guard linked else { - throw AuthErrorUtils.recaptchaSDKNotLinkedError() - } - guard actionCreated else { - throw AuthErrorUtils.recaptchaActionCreationFailed() - } - if let error { - throw error - } - if token == "NO_RECAPTCHA" { - AuthLog.logInfo(code: "I-AUT000031", - message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.") - } else { - AuthLog.logInfo( - code: "I-AUT000030", - message: "reCAPTCHA token retrieval succeeded." - ) - } - return token - #endif // !(COCOAPODS || SWIFT_PACKAGE) + private func siteKey() -> String? { + if let tenantID = auth?.tenantID { + if let config = tenantConfigs[tenantID] { + return config.siteKey + } + return nil } + return agentConfig?.siteKey + } - private static var recaptchaClient: (any RCARecaptchaClientProtocol)? + private func recaptchaToken(siteKey: String, + actionString: String, + fakeToken: String) async -> (token: String, error: Error?, + linked: Bool, actionCreated: Bool) { + if let recaptchaClient { + return await retrieveToken( + actionString: actionString, + fakeToken: fakeToken, + recaptchaClient: recaptchaClient + ) + } - private func recaptchaToken(siteKey: String, - actionString: String, - fakeToken: String) async -> (token: String, error: Error?, - linked: Bool, actionCreated: Bool) { - if let recaptchaClient { + if let recaptcha = + NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type { + do { + // Note, reCAPTCHA does not support multi-tenancy, so only one site key can be used per + // runtime. + // let client = try await recaptcha.fetchClient(withSiteKey: siteKey) + let client = try await recaptcha.getClient(withSiteKey: siteKey) + recaptchaClient = client return await retrieveToken( actionString: actionString, fakeToken: fakeToken, - recaptchaClient: recaptchaClient + recaptchaClient: client ) + } catch { + return ("", error, true, true) } - - if let recaptcha = - NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type { - do { - // let client = try await recaptcha.fetchClient(withSiteKey: siteKey) - let client = try await recaptcha.getClient(withSiteKey: siteKey) - recaptchaClient = client - return await retrieveToken( - actionString: actionString, - fakeToken: fakeToken, - recaptchaClient: client - ) - } catch { - return ("", error, true, true) - } - } else { - // RecaptchaEnterprise not linked. - return ("", nil, false, false) - } + } else { + // RecaptchaEnterprise not linked. + return ("", nil, false, false) } + } - private func retrieveToken(actionString: String, - fakeToken: String, - recaptchaClient: RCARecaptchaClientProtocol) async -> (token: String, - error: Error?, - linked: Bool, - actionCreated: Bool) { - if let recaptchaAction = - NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type { - let action = recaptchaAction.init(customAction: actionString) - let token = try? await recaptchaClient.execute(withAction: action) - return (token ?? "NO_RECAPTCHA", nil, true, true) - } else { - // RecaptchaEnterprise not linked. - return ("", nil, false, false) - } + private func retrieveToken(actionString: String, + fakeToken: String, + recaptchaClient: RCARecaptchaClientProtocol) async -> (token: String, + error: Error?, + linked: Bool, + actionCreated: Bool) { + if let recaptchaAction = + NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type { + let action = recaptchaAction.init(customAction: actionString) + let token = try? await recaptchaClient.execute(withAction: action) + return (token ?? "NO_RECAPTCHA", nil, true, true) + } else { + // RecaptchaEnterprise not linked. + return ("", nil, false, false) } + } - func retrieveRecaptchaConfig(forceRefresh: Bool) async throws { - if !forceRefresh { - if let tenantID = auth?.tenantID { - if tenantConfigs[tenantID] != nil { - return - } - } else if agentConfig != nil { - return + private func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws { + var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:] + var isRecaptchaEnabled = false + if let enforcementState = response.enforcementState { + for state in enforcementState { + guard let providerString = state["provider"], + let enforcementString = state["enforcementState"], + let provider = AuthRecaptchaProvider(rawValue: providerString), + let enforcement = AuthRecaptchaEnablementStatus(rawValue: enforcementString) else { + continue // Skip to the next state in the loop } - } - - guard let auth = auth else { - throw AuthErrorUtils.error(code: .recaptchaNotEnabled, - message: "No requestConfiguration for Auth instance") - } - let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration) - let response = try await auth.backend.call(with: request) - AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.") - try await parseRecaptchaConfigFromResponse(response: response) - } - - func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws { - var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:] - var isRecaptchaEnabled = false - if let enforcementState = response.enforcementState { - for state in enforcementState { - guard let providerString = state["provider"], - let enforcementString = state["enforcementState"], - let provider = AuthRecaptchaProvider(rawValue: providerString), - let enforcement = AuthRecaptchaEnablementStatus(rawValue: enforcementString) else { - continue // Skip to the next state in the loop - } - enablementStatus[provider] = enforcement - if enforcement != .off { - isRecaptchaEnabled = true - } + enablementStatus[provider] = enforcement + if enforcement != .off { + isRecaptchaEnabled = true } } - var siteKey = "" - // Response's site key is of the format projects//keys/' - if isRecaptchaEnabled { - if let recaptchaKey = response.recaptchaKey { - let keys = recaptchaKey.components(separatedBy: "/") - if keys.count != 4 { - throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey") - } - siteKey = keys[3] + } + var siteKey = "" + // Response's site key is of the format projects//keys/' + if isRecaptchaEnabled { + if let recaptchaKey = response.recaptchaKey { + let keys = recaptchaKey.components(separatedBy: "/") + if keys.count != 4 { + throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey") } - } - let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus) - - if let tenantID = auth?.tenantID { - tenantConfigs[tenantID] = config - } else { - agentConfig = config + siteKey = keys[3] } } + let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus) - func injectRecaptchaFields(request: any AuthRPCRequest, - provider: AuthRecaptchaProvider, - action: AuthRecaptchaAction) async throws { - try await retrieveRecaptchaConfig(forceRefresh: false) - if enablementStatus(forProvider: provider) != .off { - let token = try await verify(forceRefresh: false, action: action) - request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion) - } else { - request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion) - } + if let tenantID = auth?.tenantID { + tenantConfigs[tenantID] = config + } else { + agentConfig = config } } -#endif +} diff --git a/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift b/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift index fd7291f6820..6aba1435783 100644 --- a/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift +++ b/FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift @@ -102,12 +102,13 @@ @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced */ func testVerifyPhoneNumberWithRceEnforceSuccess() async throws { - initApp(#function) + initApp( + #function, + mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse) + ) let auth = try XCTUnwrap(PhoneAuthProviderTests.auth) // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response let provider = PhoneAuthProvider.provider(auth: auth) - let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse) - AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth) rpcIssuer.rceMode = "ENFORCE" let requestExpectation = expectation(description: "verifyRequester") rpcIssuer?.verifyRequester = { request in @@ -127,8 +128,7 @@ let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: kTestPhoneNumber, retryOnInvalidAppCredential: false, - uiDelegate: nil, - recaptchaVerifier: mockVerifier + uiDelegate: nil ) XCTAssertEqual(result, kTestVerificationID) } catch { @@ -142,12 +142,10 @@ @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced */ func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws { - initApp(#function) + initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier()) let auth = try XCTUnwrap(PhoneAuthProviderTests.auth) // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response let provider = PhoneAuthProvider.provider(auth: auth) - let mockVerifier = FakeAuthRecaptchaVerifier() - AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth) rpcIssuer.rceMode = "ENFORCE" let requestExpectation = expectation(description: "verifyRequester") rpcIssuer?.verifyRequester = { request in @@ -170,8 +168,7 @@ _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: kTestPhoneNumber, retryOnInvalidAppCredential: false, - uiDelegate: nil, - recaptchaVerifier: mockVerifier + uiDelegate: nil ) // XCTAssertEqual(result, kTestVerificationID) } catch { @@ -211,11 +208,12 @@ /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in /// audit mode func testVerifyPhoneNumberWithRceAuditSuccess() async throws { - initApp(#function) + initApp( + #function, + mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse) + ) let auth = try XCTUnwrap(PhoneAuthProviderTests.auth) let provider = PhoneAuthProvider.provider(auth: auth) - let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse) - AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth) rpcIssuer.rceMode = "AUDIT" let requestExpectation = expectation(description: "verifyRequester") rpcIssuer?.verifyRequester = { request in @@ -235,8 +233,7 @@ let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: kTestPhoneNumber, retryOnInvalidAppCredential: false, - uiDelegate: nil, - recaptchaVerifier: mockVerifier + uiDelegate: nil ) XCTAssertEqual(result, kTestVerificationID) } catch { @@ -249,11 +246,9 @@ /// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in /// audit mode func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws { - initApp(#function) + initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier()) let auth = try XCTUnwrap(PhoneAuthProviderTests.auth) let provider = PhoneAuthProvider.provider(auth: auth) - let mockVerifier = FakeAuthRecaptchaVerifier() - AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth) rpcIssuer.rceMode = "AUDIT" let requestExpectation = expectation(description: "verifyRequester") rpcIssuer?.verifyRequester = { request in @@ -276,8 +271,7 @@ _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: kTestPhoneNumber, retryOnInvalidAppCredential: false, - uiDelegate: nil, - recaptchaVerifier: mockVerifier + uiDelegate: nil ) } catch { let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError @@ -530,20 +524,17 @@ } private func testRecaptchaFlowError(function: String, rceError: Error) async throws { - initApp(function) + initApp(function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(error: rceError)) let auth = try XCTUnwrap(PhoneAuthProviderTests.auth) // TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response // Mocking the output of verify() method let provider = PhoneAuthProvider.provider(auth: auth) - let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError) - AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth) rpcIssuer.rceMode = "ENFORCE" do { let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha( toPhoneNumber: kTestPhoneNumber, retryOnInvalidAppCredential: false, - uiDelegate: nil, - recaptchaVerifier: mockVerifier + uiDelegate: nil ) } catch { XCTAssertEqual((error as NSError).code, (rceError as NSError).code) @@ -852,7 +843,8 @@ bothClientAndAppID: Bool = false, testMode: Bool = false, forwardingNotification: Bool = true, - fakeToken: Bool = false) { + fakeToken: Bool = false, + mockRecaptchaVerifier: AuthRecaptchaVerifier? = nil) { let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", gcmSenderID: "00000000000000000-00000000000-000000000") options.apiKey = PhoneAuthProviderTests.kFakeAPIKey @@ -870,7 +862,15 @@ let strippedName = functionName.replacingOccurrences(of: "(", with: "") .replacingOccurrences(of: ")", with: "") FirebaseApp.configure(name: strippedName, options: options) - let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend) + let auth = if let mockRecaptchaVerifier { + Auth( + app: FirebaseApp.app(name: strippedName)!, + backend: authBackend, + recaptchaVerifier: mockRecaptchaVerifier + ) + } else { + Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend) + } kAuthGlobalWorkQueue.sync { // Wait for Auth protectedDataInitialization to finish.