AuthRecaptchaVerifier.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193
  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. import RecaptchaInterop
  17. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  18. class AuthRecaptchaConfig {
  19. let siteKey: String
  20. let enablementStatus: [String: Bool]
  21. init(siteKey: String, enablementStatus: [String: Bool]) {
  22. self.siteKey = siteKey
  23. self.enablementStatus = enablementStatus
  24. }
  25. }
  26. enum AuthRecaptchaProvider {
  27. case password
  28. }
  29. enum AuthRecaptchaAction {
  30. case defaultAction
  31. case signInWithPassword
  32. case getOobCode
  33. case signUpPassword
  34. }
  35. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  36. class AuthRecaptchaVerifier {
  37. private(set) weak var auth: Auth?
  38. private(set) var agentConfig: AuthRecaptchaConfig?
  39. private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
  40. private(set) var recaptchaClient: RCARecaptchaClientProtocol?
  41. private static let _shared = AuthRecaptchaVerifier()
  42. private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"]
  43. private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword",
  44. AuthRecaptchaAction.getOobCode: "getOobCode",
  45. AuthRecaptchaAction.signUpPassword: "signUpPassword"]
  46. private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
  47. private init() {}
  48. class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
  49. if _shared.auth != auth {
  50. _shared.agentConfig = nil
  51. _shared.tenantConfigs = [:]
  52. _shared.auth = auth
  53. }
  54. return _shared
  55. }
  56. func siteKey() -> String? {
  57. if let tenantID = auth?.tenantID {
  58. if let config = tenantConfigs[tenantID] {
  59. return config.siteKey
  60. }
  61. return nil
  62. }
  63. return agentConfig?.siteKey
  64. }
  65. func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool {
  66. guard let providerString = providerToStringMap[provider] else {
  67. return false
  68. }
  69. if let tenantID = auth?.tenantID {
  70. guard let tenantConfig = tenantConfigs[tenantID],
  71. let status = tenantConfig.enablementStatus[providerString] else {
  72. return false
  73. }
  74. return status
  75. } else {
  76. guard let agentConfig,
  77. let status = agentConfig.enablementStatus[providerString] else {
  78. return false
  79. }
  80. return status
  81. }
  82. }
  83. func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
  84. try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
  85. if recaptchaClient == nil {
  86. guard let siteKey = siteKey(),
  87. let RecaptchaClass = NSClassFromString("Recaptcha"),
  88. let recaptcha = RecaptchaClass as? any RCARecaptchaProtocol.Type else {
  89. throw AuthErrorUtils.recaptchaSDKNotLinkedError()
  90. }
  91. recaptchaClient = try await recaptcha.getClient(withSiteKey: siteKey)
  92. }
  93. return try await retrieveRecaptchaToken(withAction: action)
  94. }
  95. func retrieveRecaptchaToken(withAction action: AuthRecaptchaAction) async throws -> String {
  96. guard let actionString = actionToStringMap[action],
  97. let RecaptchaActionClass = NSClassFromString("RecaptchaAction"),
  98. let actionClass = RecaptchaActionClass as? any RCAActionProtocol.Type else {
  99. throw AuthErrorUtils.recaptchaSDKNotLinkedError()
  100. }
  101. let customAction = actionClass.init(customAction: actionString)
  102. do {
  103. let token = try await recaptchaClient?.execute(withAction: customAction)
  104. AuthLog.logInfo(code: "I-AUT000100", message: "reCAPTCHA token retrieval succeeded.")
  105. guard let token else {
  106. AuthLog.logInfo(
  107. code: "I-AUT000101",
  108. message: "reCAPTCHA token retrieval returned nil. NO_RECAPTCHA sent as the fake code."
  109. )
  110. return "NO_RECAPTCHA"
  111. }
  112. return token
  113. } catch {
  114. AuthLog.logInfo(code: "I-AUT000102",
  115. message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")
  116. return "NO_RECAPTCHA"
  117. }
  118. }
  119. func retrieveRecaptchaConfig(forceRefresh: Bool) async throws {
  120. if !forceRefresh {
  121. if let tenantID = auth?.tenantID {
  122. if tenantConfigs[tenantID] != nil {
  123. return
  124. }
  125. } else if agentConfig != nil {
  126. return
  127. }
  128. }
  129. guard let requestConfiguration = auth?.requestConfiguration else {
  130. throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
  131. message: "No requestConfiguration for Auth instance")
  132. }
  133. let request = GetRecaptchaConfigRequest(requestConfiguration: requestConfiguration)
  134. let response = try await AuthBackend.call(with: request)
  135. AuthLog.logInfo(code: "I-AUT000103", message: "reCAPTCHA config retrieval succeeded.")
  136. // Response's site key is of the format projects/<project-id>/keys/<site-key>'
  137. guard let keys = response.recaptchaKey?.components(separatedBy: "/"),
  138. keys.count == 4 else {
  139. throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
  140. }
  141. let siteKey = keys[3]
  142. var enablementStatus: [String: Bool] = [:]
  143. if let enforcementState = response.enforcementState {
  144. for state in enforcementState {
  145. if let provider = state["provider"],
  146. provider == providerToStringMap[AuthRecaptchaProvider.password] {
  147. if let enforcement = state["enforcementState"] {
  148. if enforcement == "ENFORCE" || enforcement == "AUDIT" {
  149. enablementStatus[provider] = true
  150. } else if enforcement == "OFF" {
  151. enablementStatus[provider] = false
  152. }
  153. }
  154. }
  155. }
  156. }
  157. let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
  158. if let tenantID = auth?.tenantID {
  159. tenantConfigs[tenantID] = config
  160. } else {
  161. agentConfig = config
  162. }
  163. }
  164. func injectRecaptchaFields(request: any AuthRPCRequest,
  165. provider: AuthRecaptchaProvider,
  166. action: AuthRecaptchaAction) async throws {
  167. try await retrieveRecaptchaConfig(forceRefresh: false)
  168. if enablementStatus(forProvider: provider) {
  169. let token = try await verify(forceRefresh: false, action: action)
  170. request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
  171. } else {
  172. request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
  173. }
  174. }
  175. }
  176. #endif