Skip to content

Commit

Permalink
[Auth] Alternative recaptcha approach
Browse files Browse the repository at this point in the history
  • Loading branch information
ncooke3 committed Dec 5, 2024
1 parent d672490 commit 1a591ed
Show file tree
Hide file tree
Showing 3 changed files with 63 additions and 90 deletions.
96 changes: 28 additions & 68 deletions FirebaseAuth/Sources/ObjC/FIRRecaptchaBridge.m
Original file line number Diff line number Diff line change
Expand Up @@ -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<RCARecaptchaClientProtocol> 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<RCAActionProtocol> 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<RCARecaptchaClientProtocol> _Nullable result,
NSError *_Nullable error)) {
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,
(void *)[recaptchaClass methodForSelector:selector];
funcWithoutTimeout(recaptchaClass, selector, siteKey,
^(id<RCARecaptchaClientProtocol> _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<RCAActionProtocol> _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<RCAActionProtocol> customAction =
funcWithCustomAction([klass alloc], customActionSelector, actionString);
return customAction;
} else {
return nil;
}
}

#endif
23 changes: 11 additions & 12 deletions FirebaseAuth/Sources/Public/FirebaseAuth/FIRRecaptchaBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -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<RCARecaptchaClientProtocol> _Nullable result,
NSError *_Nullable error));

id<RCAActionProtocol> _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
34 changes: 24 additions & 10 deletions FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down

0 comments on commit 1a591ed

Please sign in to comment.