| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- // 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 {
- let siteKey: String
- let enablementStatus: [String: Bool]
- init(siteKey: String, enablementStatus: [String: Bool]) {
- self.siteKey = siteKey
- self.enablementStatus = enablementStatus
- }
- }
- enum AuthRecaptchaProvider {
- case password
- }
- enum AuthRecaptchaAction {
- case defaultAction
- case signInWithPassword
- case getOobCode
- case signUpPassword
- }
- @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 let _shared = AuthRecaptchaVerifier()
- private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"]
- private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword",
- AuthRecaptchaAction.getOobCode: "getOobCode",
- AuthRecaptchaAction.signUpPassword: "signUpPassword"]
- private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
- private init() {}
- class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
- if _shared.auth != auth {
- _shared.agentConfig = nil
- _shared.tenantConfigs = [:]
- _shared.auth = auth
- }
- return _shared
- }
- func siteKey() -> String? {
- if let tenantID = auth?.tenantID {
- if let config = tenantConfigs[tenantID] {
- return config.siteKey
- }
- return nil
- }
- return agentConfig?.siteKey
- }
- func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool {
- guard let providerString = providerToStringMap[provider] else {
- return false
- }
- if let tenantID = auth?.tenantID {
- guard let tenantConfig = tenantConfigs[tenantID],
- let status = tenantConfig.enablementStatus[providerString] else {
- return false
- }
- return status
- } else {
- guard let agentConfig,
- let status = agentConfig.enablementStatus[providerString] else {
- return false
- }
- return status
- }
- }
- func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
- try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
- guard let siteKey = siteKey() else {
- throw AuthErrorUtils.recaptchaSiteKeyMissing()
- }
- let actionString = actionToStringMap[action] ?? ""
- #if !(COCOAPODS || SWIFT_PACKAGE)
- // No recaptcha on internal build system.
- return actionString
- #else
- return try await withCheckedThrowingContinuation { continuation in
- FIRRecaptchaGetToken(siteKey, actionString,
- "NO_RECAPTCHA") { (token: String, error: Error?,
- linked: Bool, actionCreated: Bool) in
- guard linked else {
- continuation.resume(throwing: AuthErrorUtils.recaptchaSDKNotLinkedError())
- return
- }
- guard actionCreated else {
- continuation.resume(throwing: AuthErrorUtils.recaptchaActionCreationFailed())
- return
- }
- if let error {
- continuation.resume(throwing: error)
- return
- } else {
- 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."
- )
- }
- continuation.resume(returning: 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 requestConfiguration = auth?.requestConfiguration else {
- throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
- message: "No requestConfiguration for Auth instance")
- }
- let request = GetRecaptchaConfigRequest(requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
- // Response's site key is of the format projects/<project-id>/keys/<site-key>'
- guard let keys = response.recaptchaKey?.components(separatedBy: "/"),
- keys.count == 4 else {
- throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
- }
- let siteKey = keys[3]
- var enablementStatus: [String: Bool] = [:]
- if let enforcementState = response.enforcementState {
- for state in enforcementState {
- if let provider = state["provider"],
- provider == providerToStringMap[AuthRecaptchaProvider.password] {
- if let enforcement = state["enforcementState"] {
- if enforcement == "ENFORCE" || enforcement == "AUDIT" {
- enablementStatus[provider] = true
- } else if enforcement == "OFF" {
- enablementStatus[provider] = false
- }
- }
- }
- }
- }
- let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
- if let tenantID = auth?.tenantID {
- tenantConfigs[tenantID] = config
- } else {
- agentConfig = config
- }
- }
- func injectRecaptchaFields(request: any AuthRPCRequest,
- provider: AuthRecaptchaProvider,
- action: AuthRecaptchaAction) async throws {
- try await retrieveRecaptchaConfig(forceRefresh: false)
- if enablementStatus(forProvider: provider) {
- let token = try await verify(forceRefresh: false, action: action)
- request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
- } else {
- request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
- }
- }
- }
- #endif
|