Skip to content

Commit 64f5bcf

Browse files
committed
[Auth] Swift 6 improvesments for RecaptchaVerifier
1 parent e991148 commit 64f5bcf

File tree

4 files changed

+43
-72
lines changed

4 files changed

+43
-72
lines changed

FirebaseAuth/Sources/Swift/Auth/Auth.swift

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1315,15 +1315,19 @@ extension Auth: AuthInterop {
13151315
}
13161316
}
13171317

1318+
let recaptchaVerifier: AuthRecaptchaVerifier
1319+
13181320
/// Initializes reCAPTCHA using the settings configured for the project or tenant.
13191321
///
13201322
/// If you change the tenant ID of the `Auth` instance, the configuration will be
13211323
/// reloaded.
13221324
open func initializeRecaptchaConfig() async throws {
13231325
// Trigger recaptcha verification flow to initialize the recaptcha client and
13241326
// config. Recaptcha token will be returned.
1325-
let verifier = AuthRecaptchaVerifier.shared(auth: self)
1326-
_ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction)
1327+
_ = try await recaptchaVerifier.verify(
1328+
forceRefresh: true,
1329+
action: AuthRecaptchaAction.defaultAction
1330+
)
13271331
}
13281332
#endif
13291333

@@ -1623,7 +1627,8 @@ extension Auth: AuthInterop {
16231627
init(app: FirebaseApp,
16241628
keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal(),
16251629
backend: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()),
1626-
authDispatcher: AuthDispatcher = .init()) {
1630+
authDispatcher: AuthDispatcher = .init(),
1631+
recaptchaVerifier: AuthRecaptchaVerifier = .init()) {
16271632
self.app = app
16281633
mainBundleUrlTypes = Bundle.main
16291634
.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]]
@@ -1649,6 +1654,7 @@ extension Auth: AuthInterop {
16491654
appCheck: appCheck)
16501655
self.backend = backend
16511656
self.authDispatcher = authDispatcher
1657+
self.recaptchaVerifier = recaptchaVerifier
16521658

16531659
let keychainServiceName = Auth.keychainServiceName(for: app)
16541660
keychainServices = AuthKeychainServices(service: keychainServiceName,
@@ -1660,6 +1666,7 @@ extension Auth: AuthInterop {
16601666

16611667
super.init()
16621668
requestConfiguration.auth = self
1669+
self.recaptchaVerifier.auth = self
16631670

16641671
protectedDataInitialization()
16651672
}
@@ -2306,7 +2313,6 @@ extension Auth: AuthInterop {
23062313
func injectRecaptcha<T: AuthRPCRequest>(request: T,
23072314
action: AuthRecaptchaAction) async throws -> T
23082315
.Response {
2309-
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self)
23102316
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) != .off {
23112317
try await recaptchaVerifier.injectRecaptchaFields(request: request,
23122318
provider: AuthRecaptchaProvider.password,

FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,9 @@ import Foundation
202202
throw AuthErrorUtils.notificationNotForwardedError()
203203
}
204204

205-
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
206-
try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)
205+
try await auth.recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)
207206

208-
switch recaptchaVerifier.enablementStatus(forProvider: .phone) {
207+
switch auth.recaptchaVerifier.enablementStatus(forProvider: .phone) {
209208
case .off:
210209
return try await verifyClAndSendVerificationCode(
211210
toPhoneNumber: phoneNumber,
@@ -218,31 +217,28 @@ import Foundation
218217
toPhoneNumber: phoneNumber,
219218
retryOnInvalidAppCredential: true,
220219
multiFactorSession: multiFactorSession,
221-
uiDelegate: uiDelegate,
222-
recaptchaVerifier: recaptchaVerifier
220+
uiDelegate: uiDelegate
223221
)
224222
case .enforce:
225223
return try await verifyClAndSendVerificationCodeWithRecaptcha(
226224
toPhoneNumber: phoneNumber,
227225
retryOnInvalidAppCredential: false,
228226
multiFactorSession: multiFactorSession,
229-
uiDelegate: uiDelegate,
230-
recaptchaVerifier: recaptchaVerifier
227+
uiDelegate: uiDelegate
231228
)
232229
}
233230
}
234231

235232
func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
236233
retryOnInvalidAppCredential: Bool,
237-
uiDelegate: AuthUIDelegate?,
238-
recaptchaVerifier: AuthRecaptchaVerifier) async throws
234+
uiDelegate: AuthUIDelegate?) async throws
239235
-> String? {
240236
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
241237
codeIdentity: CodeIdentity.empty,
242238
requestConfiguration: auth
243239
.requestConfiguration)
244240
do {
245-
try await recaptchaVerifier.injectRecaptchaFields(
241+
try await auth.recaptchaVerifier.injectRecaptchaFields(
246242
request: request,
247243
provider: .phone,
248244
action: .sendVerificationCode
@@ -304,8 +300,7 @@ import Foundation
304300
private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
305301
retryOnInvalidAppCredential: Bool,
306302
multiFactorSession session: MultiFactorSession?,
307-
uiDelegate: AuthUIDelegate?,
308-
recaptchaVerifier: AuthRecaptchaVerifier) async throws
303+
uiDelegate: AuthUIDelegate?) async throws
309304
-> String? {
310305
if let settings = auth.settings,
311306
settings.isAppVerificationDisabledForTesting {
@@ -321,8 +316,7 @@ import Foundation
321316
return try await verifyClAndSendVerificationCodeWithRecaptcha(
322317
toPhoneNumber: phoneNumber,
323318
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
324-
uiDelegate: uiDelegate,
325-
recaptchaVerifier: recaptchaVerifier
319+
uiDelegate: uiDelegate
326320
)
327321
}
328322
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
@@ -332,7 +326,7 @@ import Foundation
332326
let request = StartMFAEnrollmentRequest(idToken: idToken,
333327
enrollmentInfo: startMFARequestInfo,
334328
requestConfiguration: auth.requestConfiguration)
335-
try await recaptchaVerifier.injectRecaptchaFields(
329+
try await auth.recaptchaVerifier.injectRecaptchaFields(
336330
request: request,
337331
provider: .phone,
338332
action: .mfaSmsEnrollment
@@ -344,7 +338,7 @@ import Foundation
344338
MFAEnrollmentID: session.multiFactorInfo?.uid,
345339
signInInfo: startMFARequestInfo,
346340
requestConfiguration: auth.requestConfiguration)
347-
try await recaptchaVerifier.injectRecaptchaFields(
341+
try await auth.recaptchaVerifier.injectRecaptchaFields(
348342
request: request,
349343
provider: .phone,
350344
action: .mfaSmsSignIn
@@ -627,7 +621,6 @@ import Foundation
627621
private let auth: Auth
628622
private let callbackScheme: String
629623
private let usingClientIDScheme: Bool
630-
private var recaptchaVerifier: AuthRecaptchaVerifier?
631624

632625
init(auth: Auth) {
633626
self.auth = auth
@@ -648,7 +641,6 @@ import Foundation
648641
return
649642
}
650643
callbackScheme = ""
651-
recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
652644
}
653645

654646
private let kAuthTypeVerifyApp = "verifyApp"

FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -68,29 +68,13 @@
6868

6969
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
7070
class AuthRecaptchaVerifier {
71-
private(set) weak var auth: Auth?
72-
private(set) var agentConfig: AuthRecaptchaConfig?
73-
private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
74-
private(set) var recaptchaClient: RCARecaptchaClientProtocol?
75-
private static var _shared = AuthRecaptchaVerifier()
71+
weak var auth: Auth?
72+
private var agentConfig: AuthRecaptchaConfig?
73+
private var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
74+
private var recaptchaClient: RCARecaptchaClientProtocol?
7675
private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
7776
init() {}
7877

79-
class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
80-
if _shared.auth != auth {
81-
_shared.agentConfig = nil
82-
_shared.tenantConfigs = [:]
83-
_shared.auth = auth
84-
}
85-
return _shared
86-
}
87-
88-
/// This function is only for testing.
89-
class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) {
90-
_shared = instance
91-
_ = shared(auth: auth)
92-
}
93-
9478
func siteKey() -> String? {
9579
if let tenantID = auth?.tenantID {
9680
if let config = tenantConfigs[tenantID] {
@@ -125,7 +109,6 @@
125109
// No recaptcha on internal build system.
126110
return actionString
127111
#else
128-
129112
let (token, error, linked, actionCreated) = await recaptchaToken(
130113
siteKey: siteKey,
131114
actionString: actionString,
@@ -154,8 +137,6 @@
154137
#endif // !(COCOAPODS || SWIFT_PACKAGE)
155138
}
156139

157-
private static var recaptchaClient: (any RCARecaptchaClientProtocol)?
158-
159140
private func recaptchaToken(siteKey: String,
160141
actionString: String,
161142
fakeToken: String) async -> (token: String, error: Error?,
@@ -171,6 +152,8 @@
171152
if let recaptcha =
172153
NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type {
173154
do {
155+
// Note, reCAPTCHA does not support multi-tenancy, so only one site key can be used per
156+
// runtime.
174157
// let client = try await recaptcha.fetchClient(withSiteKey: siteKey)
175158
let client = try await recaptcha.getClient(withSiteKey: siteKey)
176159
recaptchaClient = client

FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift

Lines changed: 17 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -102,12 +102,10 @@
102102
@brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
103103
*/
104104
func testVerifyPhoneNumberWithRceEnforceSuccess() async throws {
105-
initApp(#function)
105+
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse))
106106
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
107107
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
108108
let provider = PhoneAuthProvider.provider(auth: auth)
109-
let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
110-
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
111109
rpcIssuer.rceMode = "ENFORCE"
112110
let requestExpectation = expectation(description: "verifyRequester")
113111
rpcIssuer?.verifyRequester = { request in
@@ -127,8 +125,7 @@
127125
let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
128126
toPhoneNumber: kTestPhoneNumber,
129127
retryOnInvalidAppCredential: false,
130-
uiDelegate: nil,
131-
recaptchaVerifier: mockVerifier
128+
uiDelegate: nil
132129
)
133130
XCTAssertEqual(result, kTestVerificationID)
134131
} catch {
@@ -142,12 +139,10 @@
142139
@brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
143140
*/
144141
func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws {
145-
initApp(#function)
142+
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier())
146143
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
147144
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
148145
let provider = PhoneAuthProvider.provider(auth: auth)
149-
let mockVerifier = FakeAuthRecaptchaVerifier()
150-
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
151146
rpcIssuer.rceMode = "ENFORCE"
152147
let requestExpectation = expectation(description: "verifyRequester")
153148
rpcIssuer?.verifyRequester = { request in
@@ -170,8 +165,7 @@
170165
_ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
171166
toPhoneNumber: kTestPhoneNumber,
172167
retryOnInvalidAppCredential: false,
173-
uiDelegate: nil,
174-
recaptchaVerifier: mockVerifier
168+
uiDelegate: nil
175169
)
176170
// XCTAssertEqual(result, kTestVerificationID)
177171
} catch {
@@ -211,11 +205,9 @@
211205
/// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
212206
/// audit mode
213207
func testVerifyPhoneNumberWithRceAuditSuccess() async throws {
214-
initApp(#function)
208+
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse))
215209
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
216210
let provider = PhoneAuthProvider.provider(auth: auth)
217-
let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
218-
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
219211
rpcIssuer.rceMode = "AUDIT"
220212
let requestExpectation = expectation(description: "verifyRequester")
221213
rpcIssuer?.verifyRequester = { request in
@@ -235,8 +227,7 @@
235227
let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
236228
toPhoneNumber: kTestPhoneNumber,
237229
retryOnInvalidAppCredential: false,
238-
uiDelegate: nil,
239-
recaptchaVerifier: mockVerifier
230+
uiDelegate: nil
240231
)
241232
XCTAssertEqual(result, kTestVerificationID)
242233
} catch {
@@ -249,11 +240,9 @@
249240
/// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
250241
/// audit mode
251242
func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws {
252-
initApp(#function)
243+
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier())
253244
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
254245
let provider = PhoneAuthProvider.provider(auth: auth)
255-
let mockVerifier = FakeAuthRecaptchaVerifier()
256-
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
257246
rpcIssuer.rceMode = "AUDIT"
258247
let requestExpectation = expectation(description: "verifyRequester")
259248
rpcIssuer?.verifyRequester = { request in
@@ -276,8 +265,7 @@
276265
_ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
277266
toPhoneNumber: kTestPhoneNumber,
278267
retryOnInvalidAppCredential: false,
279-
uiDelegate: nil,
280-
recaptchaVerifier: mockVerifier
268+
uiDelegate: nil
281269
)
282270
} catch {
283271
let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
@@ -530,20 +518,17 @@
530518
}
531519

532520
private func testRecaptchaFlowError(function: String, rceError: Error) async throws {
533-
initApp(function)
521+
initApp(function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(error: rceError))
534522
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
535523
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
536524
// Mocking the output of verify() method
537525
let provider = PhoneAuthProvider.provider(auth: auth)
538-
let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError)
539-
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
540526
rpcIssuer.rceMode = "ENFORCE"
541527
do {
542528
let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
543529
toPhoneNumber: kTestPhoneNumber,
544530
retryOnInvalidAppCredential: false,
545-
uiDelegate: nil,
546-
recaptchaVerifier: mockVerifier
531+
uiDelegate: nil
547532
)
548533
} catch {
549534
XCTAssertEqual((error as NSError).code, (rceError as NSError).code)
@@ -852,7 +837,8 @@
852837
bothClientAndAppID: Bool = false,
853838
testMode: Bool = false,
854839
forwardingNotification: Bool = true,
855-
fakeToken: Bool = false) {
840+
fakeToken: Bool = false,
841+
mockRecaptchaVerifier: AuthRecaptchaVerifier? = nil) {
856842
let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
857843
gcmSenderID: "00000000000000000-00000000000-000000000")
858844
options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
@@ -870,7 +856,11 @@
870856
let strippedName = functionName.replacingOccurrences(of: "(", with: "")
871857
.replacingOccurrences(of: ")", with: "")
872858
FirebaseApp.configure(name: strippedName, options: options)
873-
let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
859+
let auth = if let mockRecaptchaVerifier {
860+
Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
861+
} else {
862+
Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
863+
}
874864

875865
kAuthGlobalWorkQueue.sync {
876866
// Wait for Auth protectedDataInitialization to finish.

0 commit comments

Comments
 (0)