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

This commit is contained in:
Nick Cooke
2025-02-03 11:40:26 -05:00
committed by GitHub
parent 0fbc5385ee
commit cae7c382db
7 changed files with 81 additions and 152 deletions
+1 -1
View File
@@ -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
View File
@@ -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.
@@ -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
@@ -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
@@ -26,5 +26,4 @@
#import "FIRGoogleAuthProvider.h"
#import "FIRMultiFactor.h"
#import "FIRPhoneAuthProvider.h"
#import "FIRRecaptchaBridge.h"
#import "FIRTwitterAuthProvider.h"
@@ -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
View File
@@ -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"),