Ver código fonte

[Auth] Add support for upcoming Recaptcha changes (#14201)

Nick Cooke 1 ano atrás
pai
commit
cae7c382db

+ 1 - 1
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.

+ 4 - 0
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.

+ 0 - 88
FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m

@@ -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 <TargetConditionals.h>
-#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<RCARecaptchaClientProtocol> 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<RCAActionProtocol> 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<RCARecaptchaClientProtocol> _Nullable recaptchaClient,
-                                        NSError *_Nullable error)) =
-        (void *)[RecaptchaClass methodForSelector:selector];
-    funcWithoutTimeout(RecaptchaClass, selector, siteKey,
-                       ^(id<RCARecaptchaClientProtocol> _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

+ 0 - 33
FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h

@@ -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 <Foundation/Foundation.h>
-#import <TargetConditionals.h>
-
-#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

+ 0 - 1
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"

+ 75 - 28
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 {

+ 1 - 1
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"),