Skip to content

Commit

Permalink
[Auth] Swift 6 improvesments for RecaptchaVerifier
Browse files Browse the repository at this point in the history
  • Loading branch information
ncooke3 committed Dec 10, 2024
1 parent e991148 commit 64f5bcf
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 72 deletions.
14 changes: 10 additions & 4 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1315,15 +1315,19 @@ extension Auth: AuthInterop {
}
}

let recaptchaVerifier: AuthRecaptchaVerifier

/// Initializes reCAPTCHA using the settings configured for the project or tenant.
///
/// If you change the tenant ID of the `Auth` instance, the configuration will be
/// reloaded.
open func initializeRecaptchaConfig() async throws {
// Trigger recaptcha verification flow to initialize the recaptcha client and
// config. Recaptcha token will be returned.
let verifier = AuthRecaptchaVerifier.shared(auth: self)
_ = try await verifier.verify(forceRefresh: true, action: AuthRecaptchaAction.defaultAction)
_ = try await recaptchaVerifier.verify(
forceRefresh: true,
action: AuthRecaptchaAction.defaultAction
)
}
#endif

Expand Down Expand Up @@ -1623,7 +1627,8 @@ extension Auth: AuthInterop {
init(app: FirebaseApp,
keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal(),
backend: AuthBackend = .init(rpcIssuer: AuthBackendRPCIssuer()),
authDispatcher: AuthDispatcher = .init()) {
authDispatcher: AuthDispatcher = .init(),
recaptchaVerifier: AuthRecaptchaVerifier = .init()) {
self.app = app
mainBundleUrlTypes = Bundle.main
.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]]
Expand All @@ -1649,6 +1654,7 @@ extension Auth: AuthInterop {
appCheck: appCheck)
self.backend = backend
self.authDispatcher = authDispatcher
self.recaptchaVerifier = recaptchaVerifier

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

super.init()
requestConfiguration.auth = self
self.recaptchaVerifier.auth = self

protectedDataInitialization()
}
Expand Down Expand Up @@ -2306,7 +2313,6 @@ extension Auth: AuthInterop {
func injectRecaptcha<T: AuthRPCRequest>(request: T,
action: AuthRecaptchaAction) async throws -> T
.Response {
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: self)
if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) != .off {
try await recaptchaVerifier.injectRecaptchaFields(request: request,
provider: AuthRecaptchaProvider.password,
Expand Down
28 changes: 10 additions & 18 deletions FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,10 +202,9 @@ import Foundation
throw AuthErrorUtils.notificationNotForwardedError()
}

let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)
try await auth.recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)

switch recaptchaVerifier.enablementStatus(forProvider: .phone) {
switch auth.recaptchaVerifier.enablementStatus(forProvider: .phone) {
case .off:
return try await verifyClAndSendVerificationCode(
toPhoneNumber: phoneNumber,
Expand All @@ -218,31 +217,28 @@ import Foundation
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
uiDelegate: uiDelegate
)
case .enforce:
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: false,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
uiDelegate: uiDelegate
)
}
}

func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
uiDelegate: AuthUIDelegate?) async throws
-> String? {
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth
.requestConfiguration)
do {
try await recaptchaVerifier.injectRecaptchaFields(
try await auth.recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .sendVerificationCode
Expand Down Expand Up @@ -304,8 +300,7 @@ import Foundation
private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
uiDelegate: AuthUIDelegate?) async throws
-> String? {
if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
Expand All @@ -321,8 +316,7 @@ import Foundation
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
uiDelegate: uiDelegate
)
}
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
Expand All @@ -332,7 +326,7 @@ import Foundation
let request = StartMFAEnrollmentRequest(idToken: idToken,
enrollmentInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
try await auth.recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .mfaSmsEnrollment
Expand All @@ -344,7 +338,7 @@ import Foundation
MFAEnrollmentID: session.multiFactorInfo?.uid,
signInInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
try await auth.recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .mfaSmsSignIn
Expand Down Expand Up @@ -627,7 +621,6 @@ import Foundation
private let auth: Auth
private let callbackScheme: String
private let usingClientIDScheme: Bool
private var recaptchaVerifier: AuthRecaptchaVerifier?

init(auth: Auth) {
self.auth = auth
Expand All @@ -648,7 +641,6 @@ import Foundation
return
}
callbackScheme = ""
recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
}

private let kAuthTypeVerifyApp = "verifyApp"
Expand Down
29 changes: 6 additions & 23 deletions FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,29 +68,13 @@

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthRecaptchaVerifier {
private(set) weak var auth: Auth?
private(set) var agentConfig: AuthRecaptchaConfig?
private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
private(set) var recaptchaClient: RCARecaptchaClientProtocol?
private static var _shared = AuthRecaptchaVerifier()
weak var auth: Auth?
private var agentConfig: AuthRecaptchaConfig?
private var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
private var recaptchaClient: RCARecaptchaClientProtocol?
private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
init() {}

class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
if _shared.auth != auth {
_shared.agentConfig = nil
_shared.tenantConfigs = [:]
_shared.auth = auth
}
return _shared
}

/// This function is only for testing.
class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) {
_shared = instance
_ = shared(auth: auth)
}

func siteKey() -> String? {
if let tenantID = auth?.tenantID {
if let config = tenantConfigs[tenantID] {
Expand Down Expand Up @@ -125,7 +109,6 @@
// No recaptcha on internal build system.
return actionString
#else

let (token, error, linked, actionCreated) = await recaptchaToken(
siteKey: siteKey,
actionString: actionString,
Expand Down Expand Up @@ -154,8 +137,6 @@
#endif // !(COCOAPODS || SWIFT_PACKAGE)
}

private static var recaptchaClient: (any RCARecaptchaClientProtocol)?

private func recaptchaToken(siteKey: String,
actionString: String,
fakeToken: String) async -> (token: String, error: Error?,
Expand All @@ -171,6 +152,8 @@
if let recaptcha =
NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type {
do {
// Note, reCAPTCHA does not support multi-tenancy, so only one site key can be used per
// runtime.
// let client = try await recaptcha.fetchClient(withSiteKey: siteKey)
let client = try await recaptcha.getClient(withSiteKey: siteKey)
recaptchaClient = client
Expand Down
44 changes: 17 additions & 27 deletions FirebaseAuth/Tests/Unit/PhoneAuthProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,12 +102,10 @@
@brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
*/
func testVerifyPhoneNumberWithRceEnforceSuccess() async throws {
initApp(#function)
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse))
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
let provider = PhoneAuthProvider.provider(auth: auth)
let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
rpcIssuer.rceMode = "ENFORCE"
let requestExpectation = expectation(description: "verifyRequester")
rpcIssuer?.verifyRequester = { request in
Expand All @@ -127,8 +125,7 @@
let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: kTestPhoneNumber,
retryOnInvalidAppCredential: false,
uiDelegate: nil,
recaptchaVerifier: mockVerifier
uiDelegate: nil
)
XCTAssertEqual(result, kTestVerificationID)
} catch {
Expand All @@ -142,12 +139,10 @@
@brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise enforced
*/
func testVerifyPhoneNumberWithRceEnforceInvalidRecaptcha() async throws {
initApp(#function)
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier())
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
let provider = PhoneAuthProvider.provider(auth: auth)
let mockVerifier = FakeAuthRecaptchaVerifier()
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
rpcIssuer.rceMode = "ENFORCE"
let requestExpectation = expectation(description: "verifyRequester")
rpcIssuer?.verifyRequester = { request in
Expand All @@ -170,8 +165,7 @@
_ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: kTestPhoneNumber,
retryOnInvalidAppCredential: false,
uiDelegate: nil,
recaptchaVerifier: mockVerifier
uiDelegate: nil
)
// XCTAssertEqual(result, kTestVerificationID)
} catch {
Expand Down Expand Up @@ -211,11 +205,9 @@
/// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
/// audit mode
func testVerifyPhoneNumberWithRceAuditSuccess() async throws {
initApp(#function)
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse))
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
let provider = PhoneAuthProvider.provider(auth: auth)
let mockVerifier = FakeAuthRecaptchaVerifier(captchaResponse: kCaptchaResponse)
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
rpcIssuer.rceMode = "AUDIT"
let requestExpectation = expectation(description: "verifyRequester")
rpcIssuer?.verifyRequester = { request in
Expand All @@ -235,8 +227,7 @@
let result = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: kTestPhoneNumber,
retryOnInvalidAppCredential: false,
uiDelegate: nil,
recaptchaVerifier: mockVerifier
uiDelegate: nil
)
XCTAssertEqual(result, kTestVerificationID)
} catch {
Expand All @@ -249,11 +240,9 @@
/// @brief Tests a successful invocation of @c verifyPhoneNumber with recaptcha enterprise in
/// audit mode
func testVerifyPhoneNumberWithRceAuditInvalidRecaptcha() async throws {
initApp(#function)
initApp(#function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier())
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
let provider = PhoneAuthProvider.provider(auth: auth)
let mockVerifier = FakeAuthRecaptchaVerifier()
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
rpcIssuer.rceMode = "AUDIT"
let requestExpectation = expectation(description: "verifyRequester")
rpcIssuer?.verifyRequester = { request in
Expand All @@ -276,8 +265,7 @@
_ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: kTestPhoneNumber,
retryOnInvalidAppCredential: false,
uiDelegate: nil,
recaptchaVerifier: mockVerifier
uiDelegate: nil
)
} catch {
let underlyingError = (error as NSError).userInfo[NSUnderlyingErrorKey] as? NSError
Expand Down Expand Up @@ -530,20 +518,17 @@
}

private func testRecaptchaFlowError(function: String, rceError: Error) async throws {
initApp(function)
initApp(function, mockRecaptchaVerifier: FakeAuthRecaptchaVerifier(error: rceError))
let auth = try XCTUnwrap(PhoneAuthProviderTests.auth)
// TODO: Figure out how to mock objective C's FIRRecaptchaGetToken response
// Mocking the output of verify() method
let provider = PhoneAuthProvider.provider(auth: auth)
let mockVerifier = FakeAuthRecaptchaVerifier(error: rceError)
AuthRecaptchaVerifier.setShared(mockVerifier, auth: auth)
rpcIssuer.rceMode = "ENFORCE"
do {
let _ = try await provider.verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: kTestPhoneNumber,
retryOnInvalidAppCredential: false,
uiDelegate: nil,
recaptchaVerifier: mockVerifier
uiDelegate: nil
)
} catch {
XCTAssertEqual((error as NSError).code, (rceError as NSError).code)
Expand Down Expand Up @@ -852,7 +837,8 @@
bothClientAndAppID: Bool = false,
testMode: Bool = false,
forwardingNotification: Bool = true,
fakeToken: Bool = false) {
fakeToken: Bool = false,
mockRecaptchaVerifier: AuthRecaptchaVerifier? = nil) {
let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000",
gcmSenderID: "00000000000000000-00000000000-000000000")
options.apiKey = PhoneAuthProviderTests.kFakeAPIKey
Expand All @@ -870,7 +856,11 @@
let strippedName = functionName.replacingOccurrences(of: "(", with: "")
.replacingOccurrences(of: ")", with: "")
FirebaseApp.configure(name: strippedName, options: options)
let auth = Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
let auth = if let mockRecaptchaVerifier {
Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
} else {
Auth(app: FirebaseApp.app(name: strippedName)!, backend: authBackend)
}

kAuthGlobalWorkQueue.sync {
// Wait for Auth protectedDataInitialization to finish.
Expand Down

0 comments on commit 64f5bcf

Please sign in to comment.