diff --git a/FirebaseAuth.podspec b/FirebaseAuth.podspec index 2983a5432c3..1fbbf403378 100644 --- a/FirebaseAuth.podspec +++ b/FirebaseAuth.podspec @@ -63,7 +63,7 @@ supports email and password accounts, as well as several 3rd party authenticatio s.dependency 'GoogleUtilities/AppDelegateSwizzler', '~> 8.0' s.dependency 'GoogleUtilities/Environment', '~> 8.0' s.dependency 'GTMSessionFetcher/Core', '>= 3.4', '< 5.0' - s.ios.dependency 'RecaptchaInterop', '~> 100.0' + s.ios.dependency 'RecaptchaInterop', '~> 101.0' s.test_spec 'unit' do |unit_tests| unit_tests.scheme = { :code_coverage => true } # Unit tests can't run on watchOS. diff --git a/FirebaseAuth/CHANGELOG.md b/FirebaseAuth/CHANGELOG.md index edbc1c4d953..c8218751161 100644 --- a/FirebaseAuth/CHANGELOG.md +++ b/FirebaseAuth/CHANGELOG.md @@ -1,3 +1,7 @@ +# Unreleased +- [changed] Using reCAPTCHA Enterprise and Firebase Auth requires reCAPTCHA + Enterprise 18.7.0 or later. + # 11.8.0 - [added] Added `ActionCodeSettings.linkDomain` to customize the Firebase Hosting link domain that is used in out-of-band email action flows. diff --git a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m b/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m deleted file mode 100644 index ebc6fa832d0..00000000000 --- a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#import -#if TARGET_OS_IOS - -#import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h" -#import "RecaptchaInterop/RecaptchaInterop.h" - -// This is thread safe since it is only called by the AuthRecaptchaVerifier singleton. -static id recaptchaClient; - -static void retrieveToken(NSString *actionString, - NSString *fakeToken, - FIRAuthRecaptchaTokenCallback callback) { - Class RecaptchaActionClass = NSClassFromString(@"RecaptchaAction"); - SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:"); - if (RecaptchaActionClass && - [RecaptchaActionClass instancesRespondToSelector:customActionSelector]) { - // Initialize with a custom action - id (*funcWithCustomAction)(id, SEL, NSString *) = (id(*)( - id, SEL, NSString *))[RecaptchaActionClass instanceMethodForSelector:customActionSelector]; - - id customAction = funcWithCustomAction([[RecaptchaActionClass alloc] init], - customActionSelector, actionString); - if (customAction) { - [recaptchaClient execute:customAction - completion:^(NSString *_Nullable token, NSError *_Nullable error) { - if (!error) { - callback(token, nil, YES, YES); - return; - } else { - callback(fakeToken, nil, YES, YES); - } - }]; - } else { - // RecaptchaAction class creation failed. - callback(@"", nil, YES, NO); - } - - } else { - // RecaptchaEnterprise not linked. - callback(@"", nil, NO, NO); - } -} - -void FIRRecaptchaGetToken(NSString *siteKey, - NSString *actionString, - NSString *fakeToken, - FIRAuthRecaptchaTokenCallback callback) { - if (recaptchaClient != nil) { - retrieveToken(actionString, fakeToken, callback); - return; - } - - Class RecaptchaClass = NSClassFromString(@"Recaptcha"); - SEL selector = NSSelectorFromString(@"getClientWithSiteKey:completion:"); - if (RecaptchaClass && [RecaptchaClass respondsToSelector:selector]) { - void (*funcWithoutTimeout)(id, SEL, NSString *, - void (^)(id _Nullable recaptchaClient, - NSError *_Nullable error)) = - (void *)[RecaptchaClass methodForSelector:selector]; - funcWithoutTimeout(RecaptchaClass, selector, siteKey, - ^(id _Nonnull client, NSError *_Nullable error) { - if (error) { - callback(@"", error, YES, YES); - } else { - recaptchaClient = client; - retrieveToken(actionString, fakeToken, callback); - } - }); - } else { - // RecaptchaEnterprise not linked. - callback(@"", nil, NO, NO); - } -} -#endif diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h deleted file mode 100644 index fd255bf43d7..00000000000 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024 Google LLC -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -#import -#import - -#if TARGET_OS_IOS - -typedef void (^FIRAuthRecaptchaTokenCallback)(NSString *_Nonnull token, - NSError *_Nullable error, - BOOL linked, - BOOL recaptchaActionCreated); - -// Provide a bridge to the Objective-C protocol provided by the optional Recaptcha Enterprise -// dependency. Once the Recaptcha Enterprise provides a Swift interop protocol, this C and -// Objective-C code can be converted to Swift. Casting to a Objective-C protocol does not seem -// possible in Swift. The C API is a workaround for linkage problems with an Objective-C API. -void FIRRecaptchaGetToken(NSString *_Nonnull siteKey, - NSString *_Nonnull actionString, - NSString *_Nonnull fakeToken, - _Nonnull FIRAuthRecaptchaTokenCallback callback); -#endif diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h index fd1aded1d6f..3917dd8e434 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FirebaseAuth.h @@ -26,5 +26,4 @@ #import "FIRGoogleAuthProvider.h" #import "FIRMultiFactor.h" #import "FIRPhoneAuthProvider.h" -#import "FIRRecaptchaBridge.h" #import "FIRTwitterAuthProvider.h" diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index 4bf7346c683..352ae35baf6 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -125,38 +125,85 @@ // No recaptcha on internal build system. return actionString #else - return try await withCheckedThrowingContinuation { continuation in - FIRRecaptchaGetToken(siteKey, actionString, - "NO_RECAPTCHA") { (token: String, error: Error?, - linked: Bool, actionCreated: Bool) in - guard linked else { - continuation.resume(throwing: AuthErrorUtils.recaptchaSDKNotLinkedError()) - return - } - guard actionCreated else { - continuation.resume(throwing: AuthErrorUtils.recaptchaActionCreationFailed()) - return - } - if let error { - continuation.resume(throwing: error) - return - } else { - 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." - ) - } - continuation.resume(returning: token) - } - } + + 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) } + 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 + ) + } + + if let recaptcha = + NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type { + do { + let client = try await recaptcha.fetchClient(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) + } + } + + 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 { diff --git a/Package.swift b/Package.swift index 08db8d760cd..499b6d8e5fe 100644 --- a/Package.swift +++ b/Package.swift @@ -168,7 +168,7 @@ let package = Package( ), .package( url: "https://github.com/google/interop-ios-for-google-sdks.git", - "100.0.0" ..< "101.0.0" + "101.0.0" ..< "102.0.0" ), .package(url: "https://github.com/google/app-check.git", "11.0.1" ..< "12.0.0"),