AuthRecaptchaVerifier.swift 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. #if os(iOS)
  15. import Foundation
  16. #if SWIFT_PACKAGE
  17. import FirebaseAuthInternal
  18. #endif
  19. import RecaptchaInterop
  20. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  21. class AuthRecaptchaConfig {
  22. let siteKey: String
  23. let enablementStatus: [String: Bool]
  24. init(siteKey: String, enablementStatus: [String: Bool]) {
  25. self.siteKey = siteKey
  26. self.enablementStatus = enablementStatus
  27. }
  28. }
  29. enum AuthRecaptchaProvider {
  30. case password
  31. }
  32. enum AuthRecaptchaAction {
  33. case defaultAction
  34. case signInWithPassword
  35. case getOobCode
  36. case signUpPassword
  37. }
  38. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  39. class AuthRecaptchaVerifier {
  40. private(set) weak var auth: Auth?
  41. private(set) var agentConfig: AuthRecaptchaConfig?
  42. private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
  43. private(set) var recaptchaClient: RCARecaptchaClientProtocol?
  44. private static let _shared = AuthRecaptchaVerifier()
  45. private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"]
  46. private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword",
  47. AuthRecaptchaAction.getOobCode: "getOobCode",
  48. AuthRecaptchaAction.signUpPassword: "signUpPassword"]
  49. private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
  50. private init() {}
  51. class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
  52. if _shared.auth != auth {
  53. _shared.agentConfig = nil
  54. _shared.tenantConfigs = [:]
  55. _shared.auth = auth
  56. }
  57. return _shared
  58. }
  59. func siteKey() -> String? {
  60. if let tenantID = auth?.tenantID {
  61. if let config = tenantConfigs[tenantID] {
  62. return config.siteKey
  63. }
  64. return nil
  65. }
  66. return agentConfig?.siteKey
  67. }
  68. func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool {
  69. guard let providerString = providerToStringMap[provider] else {
  70. return false
  71. }
  72. if let tenantID = auth?.tenantID {
  73. guard let tenantConfig = tenantConfigs[tenantID],
  74. let status = tenantConfig.enablementStatus[providerString] else {
  75. return false
  76. }
  77. return status
  78. } else {
  79. guard let agentConfig,
  80. let status = agentConfig.enablementStatus[providerString] else {
  81. return false
  82. }
  83. return status
  84. }
  85. }
  86. func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
  87. try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
  88. guard let siteKey = siteKey() else {
  89. throw AuthErrorUtils.recaptchaSiteKeyMissing()
  90. }
  91. let actionString = actionToStringMap[action] ?? ""
  92. return try await withCheckedThrowingContinuation { continuation in
  93. FIRRecaptchaGetToken(siteKey, actionString,
  94. "NO_RECAPTCHA") { (token: String, error: Error?,
  95. linked: Bool, actionCreated: Bool) in
  96. guard linked else {
  97. continuation.resume(throwing: AuthErrorUtils.recaptchaSDKNotLinkedError())
  98. return
  99. }
  100. guard actionCreated else {
  101. continuation.resume(throwing: AuthErrorUtils.recaptchaActionCreationFailed())
  102. return
  103. }
  104. if let error {
  105. continuation.resume(throwing: error)
  106. return
  107. } else {
  108. if token == "NO_RECAPTCHA" {
  109. AuthLog.logInfo(code: "I-AUT000031",
  110. message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")
  111. } else {
  112. AuthLog.logInfo(
  113. code: "I-AUT000030",
  114. message: "reCAPTCHA token retrieval succeeded."
  115. )
  116. }
  117. continuation.resume(returning: token)
  118. }
  119. }
  120. }
  121. }
  122. func retrieveRecaptchaConfig(forceRefresh: Bool) async throws {
  123. if !forceRefresh {
  124. if let tenantID = auth?.tenantID {
  125. if tenantConfigs[tenantID] != nil {
  126. return
  127. }
  128. } else if agentConfig != nil {
  129. return
  130. }
  131. }
  132. guard let requestConfiguration = auth?.requestConfiguration else {
  133. throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
  134. message: "No requestConfiguration for Auth instance")
  135. }
  136. let request = GetRecaptchaConfigRequest(requestConfiguration: requestConfiguration)
  137. let response = try await AuthBackend.call(with: request)
  138. AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
  139. // Response's site key is of the format projects/<project-id>/keys/<site-key>'
  140. guard let keys = response.recaptchaKey?.components(separatedBy: "/"),
  141. keys.count == 4 else {
  142. throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
  143. }
  144. let siteKey = keys[3]
  145. var enablementStatus: [String: Bool] = [:]
  146. if let enforcementState = response.enforcementState {
  147. for state in enforcementState {
  148. if let provider = state["provider"],
  149. provider == providerToStringMap[AuthRecaptchaProvider.password] {
  150. if let enforcement = state["enforcementState"] {
  151. if enforcement == "ENFORCE" || enforcement == "AUDIT" {
  152. enablementStatus[provider] = true
  153. } else if enforcement == "OFF" {
  154. enablementStatus[provider] = false
  155. }
  156. }
  157. }
  158. }
  159. }
  160. let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
  161. if let tenantID = auth?.tenantID {
  162. tenantConfigs[tenantID] = config
  163. } else {
  164. agentConfig = config
  165. }
  166. }
  167. func injectRecaptchaFields(request: any AuthRPCRequest,
  168. provider: AuthRecaptchaProvider,
  169. action: AuthRecaptchaAction) async throws {
  170. try await retrieveRecaptchaConfig(forceRefresh: false)
  171. if enablementStatus(forProvider: provider) {
  172. let token = try await verify(forceRefresh: false, action: action)
  173. request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
  174. } else {
  175. request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
  176. }
  177. }
  178. }
  179. #endif