Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Auth] Swift 6 improvements for AuthRecaptchaVerifier #14240

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
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