| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- // Copyright 2023 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- #if os(iOS)
- import Foundation
- #if SWIFT_PACKAGE
- import FirebaseAuthInternal
- #endif
- import RecaptchaInterop
- @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, *)
- enum AuthRecaptchaEnablementStatus: String, CaseIterable {
- case enforce = "ENFORCE"
- case audit = "AUDIT"
- case off = "OFF"
- // Convenience property for mapping values
- var stringValue: String { rawValue }
- }
- @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 }
- }
- @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
- // 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] = [:]
- // Only initialized once. Recpatcha SDK does not support multiple clients.
- private(set) var recaptchaClient: RCARecaptchaClientProtocol?
- private static var _shared = AuthRecaptchaVerifier()
- 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 enablementStatus(forProvider provider: AuthRecaptchaProvider)
- -> AuthRecaptchaEnablementStatus {
- if let tenantID = auth?.tenantID,
- let tenantConfig = tenantConfigs[tenantID],
- let status = tenantConfig.enablementStatus[provider] {
- return status
- } else if let agentConfig = agentConfig,
- let status = agentConfig.enablementStatus[provider] {
- return status
- } else {
- return AuthRecaptchaEnablementStatus.off
- }
- }
- func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
- try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
- guard let siteKey = siteKey() else {
- throw AuthErrorUtils.recaptchaSiteKeyMissing()
- }
- let actionString = action.stringValue
- #if !(COCOAPODS || SWIFT_PACKAGE)
- // No recaptcha on internal build system.
- return actionString
- #else
- let (token, error, linked, actionCreated) = await recaptchaToken(
- siteKey: siteKey,
- actionString: actionString,
- fakeToken: "NO_RECAPTCHA"
- )
- guard linked else {
- throw AuthErrorUtils.recaptchaSDKNotLinkedError()
- }
- guard actionCreated else {
- throw AuthErrorUtils.recaptchaActionCreationFailed()
- }
- if let error {
- throw error
- }
- if token == "NO_RECAPTCHA" {
- AuthLog.logInfo(code: "I-AUT000031",
- message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")
- } else {
- AuthLog.logInfo(
- code: "I-AUT000030",
- message: "reCAPTCHA token retrieval succeeded."
- )
- }
- return token
- #endif // !(COCOAPODS || SWIFT_PACKAGE)
- }
- func retrieveRecaptchaConfig(forceRefresh: Bool) async throws {
- if !forceRefresh {
- if let tenantID = auth?.tenantID {
- if tenantConfigs[tenantID] != nil {
- return
- }
- } else if agentConfig != nil {
- return
- }
- }
- guard let auth = auth else {
- throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
- message: "No requestConfiguration for Auth instance")
- }
- let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration)
- let response = try await auth.backend.call(with: request)
- AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
- try await parseRecaptchaConfigFromResponse(response: response)
- }
- private func siteKey() -> String? {
- if let tenantID = auth?.tenantID {
- if let config = tenantConfigs[tenantID] {
- return config.siteKey
- }
- return nil
- }
- return agentConfig?.siteKey
- }
- func injectRecaptchaFields(request: any AuthRPCRequest,
- provider: AuthRecaptchaProvider,
- action: AuthRecaptchaAction) async throws {
- 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)
- } else {
- request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
- }
- }
- #if COCOAPODS || SWIFT_PACKAGE // No recaptcha on internal build system.
- private func recaptchaToken(siteKey: String,
- actionString: String,
- fakeToken: String) async -> (token: String, error: Error?,
- linked: Bool, actionCreated: Bool) {
- if let recaptchaClient {
- return await retrieveToken(
- actionString: actionString,
- fakeToken: fakeToken,
- recaptchaClient: recaptchaClient
- )
- }
- if let recaptcha =
- NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type {
- do {
- let client = try await recaptcha.fetchClient(withSiteKey: siteKey)
- recaptchaClient = client
- return await retrieveToken(
- actionString: actionString,
- fakeToken: fakeToken,
- recaptchaClient: client
- )
- } catch {
- return ("", error, true, true)
- }
- } else {
- // RecaptchaEnterprise not linked.
- return ("", nil, false, false)
- }
- }
- #endif // (COCOAPODS || SWIFT_PACKAGE)
- private func retrieveToken(actionString: String,
- fakeToken: String,
- recaptchaClient: RCARecaptchaClientProtocol) async -> (token: String,
- error: Error?,
- linked: Bool,
- actionCreated: Bool) {
- 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 {
- // RecaptchaEnterprise not linked.
- return ("", nil, false, false)
- }
- }
- private func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws {
- var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:]
- var isRecaptchaEnabled = false
- if let enforcementState = response.enforcementState {
- for state in enforcementState {
- guard let providerString = state["provider"],
- let enforcementString = state["enforcementState"],
- let provider = AuthRecaptchaProvider(rawValue: providerString),
- let enforcement = AuthRecaptchaEnablementStatus(rawValue: enforcementString) else {
- continue // Skip to the next state in the loop
- }
- enablementStatus[provider] = enforcement
- if enforcement != .off {
- isRecaptchaEnabled = true
- }
- }
- }
- var siteKey = ""
- // Response's site key is of the format projects/<project-id>/keys/<site-key>'
- if isRecaptchaEnabled {
- if let recaptchaKey = response.recaptchaKey {
- let keys = recaptchaKey.components(separatedBy: "/")
- if keys.count != 4 {
- throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
- }
- siteKey = keys[3]
- }
- }
- let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
- if let tenantID = auth?.tenantID {
- tenantConfigs[tenantID] = config
- } else {
- agentConfig = config
- }
- }
- }
- #endif
|