| 
12 | 12 | // See the License for the specific language governing permissions and  | 
13 | 13 | // limitations under the License.  | 
14 | 14 | 
 
  | 
15 |  | -#if os(iOS)  | 
 | 15 | +import Foundation  | 
16 | 16 | 
 
  | 
17 |  | -  import Foundation  | 
 | 17 | +#if SWIFT_PACKAGE  | 
 | 18 | +  import FirebaseAuthInternal  | 
 | 19 | +#endif  | 
18 | 20 | 
 
  | 
19 |  | -  #if SWIFT_PACKAGE  | 
20 |  | -    import FirebaseAuthInternal  | 
21 |  | -  #endif  | 
 | 21 | +#if os(iOS)  | 
22 | 22 |   import RecaptchaInterop  | 
 | 23 | +#endif  | 
23 | 24 | 
 
  | 
24 |  | -  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
25 |  | -  class AuthRecaptchaConfig {  | 
26 |  | -    var siteKey: String?  | 
27 |  | -    let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]  | 
 | 25 | +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
 | 26 | +class AuthRecaptchaConfig {  | 
 | 27 | +  var siteKey: String?  | 
 | 28 | +  let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]  | 
28 | 29 | 
 
  | 
29 |  | -    init(siteKey: String? = nil,  | 
30 |  | -         enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {  | 
31 |  | -      self.siteKey = siteKey  | 
32 |  | -      self.enablementStatus = enablementStatus  | 
33 |  | -    }  | 
 | 30 | +  init(siteKey: String? = nil,  | 
 | 31 | +       enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {  | 
 | 32 | +    self.siteKey = siteKey  | 
 | 33 | +    self.enablementStatus = enablementStatus  | 
34 | 34 |   }  | 
 | 35 | +}  | 
35 | 36 | 
 
  | 
36 |  | -  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
37 |  | -  enum AuthRecaptchaEnablementStatus: String, CaseIterable {  | 
38 |  | -    case enforce = "ENFORCE"  | 
39 |  | -    case audit = "AUDIT"  | 
40 |  | -    case off = "OFF"  | 
 | 37 | +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
 | 38 | +enum AuthRecaptchaEnablementStatus: String, CaseIterable {  | 
 | 39 | +  case enforce = "ENFORCE"  | 
 | 40 | +  case audit = "AUDIT"  | 
 | 41 | +  case off = "OFF"  | 
41 | 42 | 
 
  | 
42 |  | -    // Convenience property for mapping values  | 
43 |  | -    var stringValue: String { rawValue }  | 
44 |  | -  }  | 
 | 43 | +  // Convenience property for mapping values  | 
 | 44 | +  var stringValue: String { rawValue }  | 
 | 45 | +}  | 
45 | 46 | 
 
  | 
46 |  | -  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
47 |  | -  enum AuthRecaptchaProvider: String, CaseIterable {  | 
48 |  | -    case password = "EMAIL_PASSWORD_PROVIDER"  | 
49 |  | -    case phone = "PHONE_PROVIDER"  | 
 | 47 | +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
 | 48 | +enum AuthRecaptchaProvider: String, CaseIterable {  | 
 | 49 | +  case password = "EMAIL_PASSWORD_PROVIDER"  | 
 | 50 | +  case phone = "PHONE_PROVIDER"  | 
50 | 51 | 
 
  | 
51 |  | -    // Convenience property for mapping values  | 
52 |  | -    var stringValue: String { rawValue }  | 
53 |  | -  }  | 
 | 52 | +  // Convenience property for mapping values  | 
 | 53 | +  var stringValue: String { rawValue }  | 
 | 54 | +}  | 
54 | 55 | 
 
  | 
55 |  | -  @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
56 |  | -  enum AuthRecaptchaAction: String {  | 
57 |  | -    case defaultAction  | 
58 |  | -    case signInWithPassword  | 
59 |  | -    case getOobCode  | 
60 |  | -    case signUpPassword  | 
61 |  | -    case sendVerificationCode  | 
62 |  | -    case mfaSmsSignIn  | 
63 |  | -    case mfaSmsEnrollment  | 
 | 56 | +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
 | 57 | +enum AuthRecaptchaAction: String {  | 
 | 58 | +  case defaultAction  | 
 | 59 | +  case signInWithPassword  | 
 | 60 | +  case getOobCode  | 
 | 61 | +  case signUpPassword  | 
 | 62 | +  case sendVerificationCode  | 
 | 63 | +  case mfaSmsSignIn  | 
 | 64 | +  case mfaSmsEnrollment  | 
 | 65 | + | 
 | 66 | +  // Convenience property for mapping values  | 
 | 67 | +  var stringValue: String { rawValue }  | 
 | 68 | +}  | 
 | 69 | + | 
 | 70 | +@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
 | 71 | +class AuthRecaptchaVerifier {  | 
 | 72 | +  weak var auth: Auth?  | 
 | 73 | +  private var agentConfig: AuthRecaptchaConfig?  | 
 | 74 | +  private var tenantConfigs: [String: AuthRecaptchaConfig] = [:]  | 
 | 75 | +  private var recaptchaClient: RCARecaptchaClientProtocol?  | 
 | 76 | +  private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"  | 
 | 77 | + | 
 | 78 | +  init() {}  | 
64 | 79 | 
 
  | 
65 |  | -    // Convenience property for mapping values  | 
66 |  | -    var stringValue: String { rawValue }  | 
 | 80 | +  func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {  | 
 | 81 | +    try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)  | 
 | 82 | +    guard let siteKey = siteKey() else {  | 
 | 83 | +      throw AuthErrorUtils.recaptchaSiteKeyMissing()  | 
 | 84 | +    }  | 
 | 85 | +    let actionString = action.stringValue  | 
 | 86 | +    #if !(COCOAPODS || SWIFT_PACKAGE)  | 
 | 87 | +      // No recaptcha on internal build system.  | 
 | 88 | +      return actionString  | 
 | 89 | +    #else  | 
 | 90 | +      let (token, error, linked, actionCreated) = await recaptchaToken(  | 
 | 91 | +        siteKey: siteKey,  | 
 | 92 | +        actionString: actionString,  | 
 | 93 | +        fakeToken: "NO_RECAPTCHA"  | 
 | 94 | +      )  | 
 | 95 | + | 
 | 96 | +      guard linked else {  | 
 | 97 | +        throw AuthErrorUtils.recaptchaSDKNotLinkedError()  | 
 | 98 | +      }  | 
 | 99 | +      guard actionCreated else {  | 
 | 100 | +        throw AuthErrorUtils.recaptchaActionCreationFailed()  | 
 | 101 | +      }  | 
 | 102 | +      if let error {  | 
 | 103 | +        throw error  | 
 | 104 | +      }  | 
 | 105 | +      if token == "NO_RECAPTCHA" {  | 
 | 106 | +        AuthLog.logInfo(code: "I-AUT000031",  | 
 | 107 | +                        message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")  | 
 | 108 | +      } else {  | 
 | 109 | +        AuthLog.logInfo(  | 
 | 110 | +          code: "I-AUT000030",  | 
 | 111 | +          message: "reCAPTCHA token retrieval succeeded."  | 
 | 112 | +        )  | 
 | 113 | +      }  | 
 | 114 | +      return token  | 
 | 115 | +    #endif // !(COCOAPODS || SWIFT_PACKAGE)  | 
67 | 116 |   }  | 
 | 117 | +}  | 
68 | 118 | 
 
  | 
 | 119 | +#if os(iOS)  | 
69 | 120 |   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)  | 
70 |  | -  class AuthRecaptchaVerifier {  | 
71 |  | -    weak var auth: Auth?  | 
72 |  | -    private var agentConfig: AuthRecaptchaConfig?  | 
73 |  | -    private var tenantConfigs: [String: AuthRecaptchaConfig] = [:]  | 
74 |  | -    private var recaptchaClient: RCARecaptchaClientProtocol?  | 
75 |  | -    private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"  | 
76 |  | -    init() {}  | 
77 |  | - | 
 | 121 | +  extension AuthRecaptchaVerifier {  | 
78 | 122 |     func siteKey() -> String? {  | 
79 | 123 |       if let tenantID = auth?.tenantID {  | 
80 | 124 |         if let config = tenantConfigs[tenantID] {  | 
 | 
99 | 143 |       }  | 
100 | 144 |     }  | 
101 | 145 | 
 
  | 
102 |  | -    func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {  | 
103 |  | -      try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)  | 
104 |  | -      guard let siteKey = siteKey() else {  | 
105 |  | -        throw AuthErrorUtils.recaptchaSiteKeyMissing()  | 
106 |  | -      }  | 
107 |  | -      let actionString = action.stringValue  | 
108 |  | -      #if !(COCOAPODS || SWIFT_PACKAGE)  | 
109 |  | -        // No recaptcha on internal build system.  | 
110 |  | -        return actionString  | 
111 |  | -      #else  | 
112 |  | -        let (token, error, linked, actionCreated) = await recaptchaToken(  | 
113 |  | -          siteKey: siteKey,  | 
114 |  | -          actionString: actionString,  | 
115 |  | -          fakeToken: "NO_RECAPTCHA"  | 
116 |  | -        )  | 
117 |  | - | 
118 |  | -        guard linked else {  | 
119 |  | -          throw AuthErrorUtils.recaptchaSDKNotLinkedError()  | 
120 |  | -        }  | 
121 |  | -        guard actionCreated else {  | 
122 |  | -          throw AuthErrorUtils.recaptchaActionCreationFailed()  | 
123 |  | -        }  | 
124 |  | -        if let error {  | 
125 |  | -          throw error  | 
126 |  | -        }  | 
127 |  | -        if token == "NO_RECAPTCHA" {  | 
128 |  | -          AuthLog.logInfo(code: "I-AUT000031",  | 
129 |  | -                          message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")  | 
130 |  | -        } else {  | 
131 |  | -          AuthLog.logInfo(  | 
132 |  | -            code: "I-AUT000030",  | 
133 |  | -            message: "reCAPTCHA token retrieval succeeded."  | 
134 |  | -          )  | 
135 |  | -        }  | 
136 |  | -        return token  | 
137 |  | -      #endif // !(COCOAPODS || SWIFT_PACKAGE)  | 
138 |  | -    }  | 
139 |  | - | 
140 | 146 |     private func recaptchaToken(siteKey: String,  | 
141 | 147 |                                 actionString: String,  | 
142 | 148 |                                 fakeToken: String) async -> (token: String, error: Error?,  | 
 | 
0 commit comments