From 1a591ed96a653e5c11153f1ba989671119f6a72c Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Thu, 5 Dec 2024 18:33:17 -0500 Subject: [PATCH 1/2] [Auth] Alternative recaptcha approach --- .../Sources/ObjC/FIRRecaptchaBridge.m | 96 ++++++------------- .../Public/FirebaseAuth/FIRRecaptchaBridge.h | 23 +++-- .../Utilities/AuthRecaptchaVerifier.swift | 34 +++++-- 3 files changed, 63 insertions(+), 90 deletions(-) diff --git a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m b/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m index c35397426c2..cf93edc3295 100644 --- a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m +++ b/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m @@ -18,83 +18,43 @@ #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(@"RecaptchaEnterprise.RCAAction"); - SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:"); - if (!RecaptchaActionClass) { - // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. - RecaptchaActionClass = NSClassFromString(@"RecaptchaAction"); - } - - 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], 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; - } - - // Why not use `conformsToProtocol`? - Class RecaptchaClass = NSClassFromString(@"RecaptchaEnterprise.RCARecaptcha"); - SEL selector = NSSelectorFromString(@"fetchClientWithSiteKey:completion:"); - if (!RecaptchaClass) { - // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. - RecaptchaClass = NSClassFromString(@"Recaptcha"); - selector = NSSelectorFromString(@"getClientWithSiteKey:completion:"); - } - - if (RecaptchaClass && [RecaptchaClass respondsToSelector:selector]) { +void __objc_getClientWithSiteKey( + NSString *siteKey, + Class recaptchaClass, + void (^completionHandler)(id _Nullable result, + NSError *_Nullable error)) { + 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, + (void *)[recaptchaClass methodForSelector:selector]; + funcWithoutTimeout(recaptchaClass, selector, siteKey, ^(id _Nonnull client, NSError *_Nullable error) { if (error) { - callback(@"", error, YES, YES); + completionHandler(nil, error); } else { - recaptchaClient = client; - retrieveToken(actionString, fakeToken, callback); + completionHandler(client, nil); } }); } else { - // RecaptchaEnterprise not linked. - callback(@"", nil, NO, NO); + completionHandler(nil, nil); // TODO(ncooke3): Add error just in case. } } + +id _Nullable __fir_initActionFromClass(Class _Nonnull klass, + NSString *_Nonnull actionString) { + SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:"); + if (klass && [klass instancesRespondToSelector:customActionSelector]) { + id (*funcWithCustomAction)(id, SEL, NSString *) = + (id(*)(id, SEL, NSString *))[klass instanceMethodForSelector:customActionSelector]; + + id customAction = + funcWithCustomAction([klass alloc], customActionSelector, actionString); + return customAction; + } else { + return nil; + } +} + #endif diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h index fd255bf43d7..bded2e63c91 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h @@ -17,17 +17,16 @@ #if TARGET_OS_IOS -typedef void (^FIRAuthRecaptchaTokenCallback)(NSString *_Nonnull token, - NSError *_Nullable error, - BOOL linked, - BOOL recaptchaActionCreated); +@protocol RCARecaptchaClientProtocol; +@protocol RCAActionProtocol; + +void __objc_getClientWithSiteKey( + NSString *_Nonnull siteKey, + Class _Nonnull recaptchaClass, + void (^_Nonnull completionHandler)(id _Nullable result, + NSError *_Nullable error)); + +id _Nullable __fir_initActionFromClass(Class _Nonnull klass, + NSString *_Nonnull actionString); -// 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/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index 09bf3b193d3..d54aa5188c2 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -177,7 +177,18 @@ } else if let recaptcha = NSClassFromString("Recaptcha") { // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. do { - let client = try await recaptcha.getClient(withSiteKey: siteKey) + let client: any RCARecaptchaClientProtocol = + try await withCheckedThrowingContinuation { continuation in + __objc_getClientWithSiteKey(siteKey, recaptcha) { client, error in + if let error { + continuation.resume(throwing: error) + } + if let client { + continuation.resume(returning: client) + } + // TODO(ncooke3): Handle other case. + } + } recaptchaClient = client return await retrieveToken(actionString: actionString, fakeToken: fakeToken) } catch { @@ -192,18 +203,21 @@ private func retrieveToken(actionString: String, fakeToken: String) async -> (token: String, error: Error?, linked: Bool, actionCreated: Bool) { - let recaptchaAction = ( - NSClassFromString("RecaptchaEnterprise.RCAAction") ?? NSClassFromString("RecaptchaAction") - ) as? RCAActionProtocol.Type - - guard let recaptchaAction else { + 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 if + let recaptchaAction = NSClassFromString("RecaptchaAction"), + let action = __fir_initActionFromClass(recaptchaAction, actionString) { + // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. + let token = try? await recaptchaClient!.execute(withAction: action) + return (token ?? "NO_RECAPTCHA", nil, true, true) + } else { // RecaptchaEnterprise not linked. return ("", nil, false, false) } - - let action = recaptchaAction.init(customAction: actionString) - let token = try? await recaptchaClient!.execute(withAction: action) - return (token ?? "NO_RECAPTCHA", nil, true, true) } func retrieveRecaptchaConfig(forceRefresh: Bool) async throws { From ae603e5565d0e9bd699cd83e6b8d8582d975b534 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 6 Dec 2024 10:34:09 -0500 Subject: [PATCH 2/2] Simplify approach --- .../Sources/ObjC/FIRRecaptchaBridge.m | 42 +++++-------------- .../Public/FirebaseAuth/FIRRecaptchaBridge.h | 12 ++---- .../Utilities/AuthRecaptchaVerifier.swift | 20 +++------ 3 files changed, 20 insertions(+), 54 deletions(-) diff --git a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m b/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m index cf93edc3295..6ed445cf54e 100644 --- a/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m +++ b/FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m @@ -18,43 +18,23 @@ #import "FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h" #import "RecaptchaInterop/RecaptchaInterop.h" -void __objc_getClientWithSiteKey( - NSString *siteKey, - Class recaptchaClass, - void (^completionHandler)(id _Nullable result, - NSError *_Nullable error)) { - 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) { - completionHandler(nil, error); - } else { - completionHandler(client, nil); - } - }); +Class _Nonnull __fir_castToRecaptchaProtocolFromClass(Class _Nonnull klass) { + if ([klass conformsToProtocol:@protocol(RCARecaptchaProtocol)]) { + NSLog(@"RCARecaptchaProtocol - true"); } else { - completionHandler(nil, nil); // TODO(ncooke3): Add error just in case. + NSLog(@"RCARecaptchaProtocol - false"); } + return (Class)klass; } -id _Nullable __fir_initActionFromClass(Class _Nonnull klass, - NSString *_Nonnull actionString) { - SEL customActionSelector = NSSelectorFromString(@"initWithCustomAction:"); - if (klass && [klass instancesRespondToSelector:customActionSelector]) { - id (*funcWithCustomAction)(id, SEL, NSString *) = - (id(*)(id, SEL, NSString *))[klass instanceMethodForSelector:customActionSelector]; - - id customAction = - funcWithCustomAction([klass alloc], customActionSelector, actionString); - return customAction; +Class _Nonnull __fir_castToRecaptchaActionProtocolFromClass( + Class _Nonnull klass) { + if ([klass conformsToProtocol:@protocol(RCAActionProtocol)]) { + NSLog(@"RCAActionProtocol - true"); } else { - return nil; + NSLog(@"RCAActionProtocol - false"); } + return (Class)klass; } #endif diff --git a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h index bded2e63c91..05de63862e0 100644 --- a/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h +++ b/FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h @@ -17,16 +17,12 @@ #if TARGET_OS_IOS -@protocol RCARecaptchaClientProtocol; +@protocol RCARecaptchaProtocol; @protocol RCAActionProtocol; -void __objc_getClientWithSiteKey( - NSString *_Nonnull siteKey, - Class _Nonnull recaptchaClass, - void (^_Nonnull completionHandler)(id _Nullable result, - NSError *_Nullable error)); +Class _Nonnull __fir_castToRecaptchaProtocolFromClass(Class _Nonnull klass); -id _Nullable __fir_initActionFromClass(Class _Nonnull klass, - NSString *_Nonnull actionString); +Class _Nonnull __fir_castToRecaptchaActionProtocolFromClass( + Class _Nonnull klass); #endif diff --git a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift index d54aa5188c2..8fc78d8ea74 100644 --- a/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift +++ b/FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift @@ -177,18 +177,8 @@ } else if let recaptcha = NSClassFromString("Recaptcha") { // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. do { - let client: any RCARecaptchaClientProtocol = - try await withCheckedThrowingContinuation { continuation in - __objc_getClientWithSiteKey(siteKey, recaptcha) { client, error in - if let error { - continuation.resume(throwing: error) - } - if let client { - continuation.resume(returning: client) - } - // TODO(ncooke3): Handle other case. - } - } + let recaptcha = __fir_castToRecaptchaProtocolFromClass(recaptcha) + let client = try await recaptcha.getClient(withSiteKey: siteKey) recaptchaClient = client return await retrieveToken(actionString: actionString, fakeToken: fakeToken) } catch { @@ -208,10 +198,10 @@ let action = recaptchaAction.init(customAction: actionString) let token = try? await recaptchaClient!.execute(withAction: action) return (token ?? "NO_RECAPTCHA", nil, true, true) - } else if - let recaptchaAction = NSClassFromString("RecaptchaAction"), - let action = __fir_initActionFromClass(recaptchaAction, actionString) { + } else if let recaptchaAction = NSClassFromString("RecaptchaAction") { // Fall back to attempting to connect with pre-18.7.0 RecaptchaEnterprise. + let recaptchaAction = __fir_castToRecaptchaActionProtocolFromClass(recaptchaAction) + let action = recaptchaAction.init(customAction: actionString) let token = try? await recaptchaClient!.execute(withAction: action) return (token ?? "NO_RECAPTCHA", nil, true, true) } else {