Skip to content
14 changes: 10 additions & 4 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,8 @@ extension Auth: AuthInterop {
return false
}

let recaptchaVerifier: AuthRecaptchaVerifier

#if os(iOS) && !targetEnvironment(macCatalyst)

/// Initializes reCAPTCHA using the settings configured for the project or tenant.
Expand Down Expand Up @@ -1326,8 +1328,10 @@ extension Auth: AuthInterop {
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 @@ -1627,7 +1631,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 @@ -1653,6 +1658,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 @@ -1664,6 +1670,7 @@ extension Auth: AuthInterop {

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

protectedDataInitialization()
}
Expand Down Expand Up @@ -2307,7 +2314,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
29 changes: 10 additions & 19 deletions FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,8 +202,6 @@ import Foundation
throw AuthErrorUtils.notificationNotForwardedError()
}

let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)

if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
// If app verification is disabled for testing
Expand All @@ -218,9 +216,9 @@ import Foundation
)
}

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 @@ -233,31 +231,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 @@ -319,8 +314,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 @@ -336,8 +330,7 @@ import Foundation
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
uiDelegate: uiDelegate
)
}
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
Expand All @@ -347,7 +340,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 @@ -359,7 +352,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 @@ -641,7 +634,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 @@ -662,7 +654,6 @@ import Foundation
return
}
callbackScheme = ""
recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
}

private let kAuthTypeVerifyApp = "verifyApp"
Expand Down
132 changes: 58 additions & 74 deletions FirebaseAuth/Sources/Swift/Utilities/AuthRecaptchaVerifier.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,86 +12,71 @@
// See the License for the specific language governing permissions and
// limitations under the License.

#if os(iOS)
import Foundation

import Foundation
#if SWIFT_PACKAGE
import FirebaseAuthInternal
#endif

#if SWIFT_PACKAGE
import FirebaseAuthInternal
#endif
#if os(iOS)
import RecaptchaInterop
#endif // os(iOS)

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthRecaptchaConfig {
var siteKey: String?
let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]

init(siteKey: String? = nil,
enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {
self.siteKey = siteKey
self.enablementStatus = enablementStatus
}
}
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthRecaptchaConfig {
var siteKey: String?
let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaEnablementStatus: String, CaseIterable {
case enforce = "ENFORCE"
case audit = "AUDIT"
case off = "OFF"

// Convenience property for mapping values
var stringValue: String { rawValue }
init(siteKey: String? = nil,
enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {
self.siteKey = siteKey
self.enablementStatus = enablementStatus
}
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaProvider: String, CaseIterable {
case password = "EMAIL_PASSWORD_PROVIDER"
case phone = "PHONE_PROVIDER"
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaEnablementStatus: String, CaseIterable {
case enforce = "ENFORCE"
case audit = "AUDIT"
case off = "OFF"

// Convenience property for mapping values
var stringValue: String { rawValue }
}
// Convenience property for mapping values
var stringValue: String { rawValue }
}

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaAction: String {
case defaultAction
case signInWithPassword
case getOobCode
case signUpPassword
case sendVerificationCode
case mfaSmsSignIn
case mfaSmsEnrollment
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaProvider: String, CaseIterable {
case password = "EMAIL_PASSWORD_PROVIDER"
case phone = "PHONE_PROVIDER"

// Convenience property for mapping values
var stringValue: String { rawValue }
}
// Convenience property for mapping values
var stringValue: String { rawValue }
}

@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()
private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
init() {}
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
enum AuthRecaptchaAction: String {
case defaultAction
case signInWithPassword
case getOobCode
case signUpPassword
case sendVerificationCode
case mfaSmsSignIn
case mfaSmsEnrollment

class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
if _shared.auth != auth {
_shared.agentConfig = nil
_shared.tenantConfigs = [:]
_shared.auth = auth
}
return _shared
}
Comment on lines -79 to -86
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the sample app, I tried the following using the current code (red diff):

  1. initialize recaptcha (success)
  2. switch to configured secondary app
  3. initialize recaptcha (threw internal error)

I tried these steps again with the new code, and I got the same result.

// Convenience property for mapping values
var stringValue: String { rawValue }
}

/// This function is only for testing.
class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) {
_shared = instance
_ = shared(auth: auth)
}
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
class AuthRecaptchaVerifier {
private let recaptchaVersion = "RECAPTCHA_ENTERPRISE"
weak var auth: Auth?
private var agentConfig: AuthRecaptchaConfig?
private var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
#if os(iOS)
private var recaptchaClient: RCARecaptchaClientProtocol?

func siteKey() -> String? {
private func siteKey() -> String? {
if let tenantID = auth?.tenantID {
if let config = tenantConfigs[tenantID] {
return config.siteKey
Expand Down Expand Up @@ -125,7 +110,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 +138,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 +153,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)
recaptchaClient = client
return await retrieveToken(
Expand Down Expand Up @@ -225,7 +209,7 @@
try await parseRecaptchaConfigFromResponse(response: response)
}

func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws {
private func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws {
var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:]
var isRecaptchaEnabled = false
if let enforcementState = response.enforcementState {
Expand Down Expand Up @@ -268,10 +252,10 @@
try await retrieveRecaptchaConfig(forceRefresh: false)
if enablementStatus(forProvider: provider) != .off {
let token = try await verify(forceRefresh: false, action: action)
request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: recaptchaVersion)
} else {
request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: recaptchaVersion)
}
}
}
#endif
#endif // os(iOS)
}
Loading
Loading