|
23 | 23 |
|
24 | 24 | @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
25 | 25 | class AuthRecaptchaConfig {
|
26 |
| - let siteKey: String |
27 |
| - let enablementStatus: [String: Bool] |
| 26 | + var siteKey: String? |
| 27 | + let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] |
28 | 28 |
|
29 |
| - init(siteKey: String, enablementStatus: [String: Bool]) { |
| 29 | + init(siteKey: String? = nil, |
| 30 | + enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) { |
30 | 31 | self.siteKey = siteKey
|
31 | 32 | self.enablementStatus = enablementStatus
|
32 | 33 | }
|
33 | 34 | }
|
34 | 35 |
|
35 |
| - enum AuthRecaptchaProvider { |
36 |
| - case password |
| 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" |
| 41 | + |
| 42 | + // Convenience property for mapping values |
| 43 | + var stringValue: String { rawValue } |
37 | 44 | }
|
38 | 45 |
|
39 |
| - enum AuthRecaptchaAction { |
| 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" |
| 50 | + |
| 51 | + // Convenience property for mapping values |
| 52 | + var stringValue: String { rawValue } |
| 53 | + } |
| 54 | + |
| 55 | + @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) |
| 56 | + enum AuthRecaptchaAction: String { |
40 | 57 | case defaultAction
|
41 | 58 | case signInWithPassword
|
42 | 59 | case getOobCode
|
43 | 60 | case signUpPassword
|
| 61 | + case sendVerificationCode |
| 62 | + case mfaSmsSignIn |
| 63 | + case mfaSmsEnrollment |
| 64 | + |
| 65 | + // Convenience property for mapping values |
| 66 | + var stringValue: String { rawValue } |
44 | 67 | }
|
45 | 68 |
|
46 | 69 | @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
49 | 72 | private(set) var agentConfig: AuthRecaptchaConfig?
|
50 | 73 | private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
|
51 | 74 | private(set) var recaptchaClient: RCARecaptchaClientProtocol?
|
52 |
| - |
53 |
| - private static let _shared = AuthRecaptchaVerifier() |
54 |
| - private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"] |
55 |
| - private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword", |
56 |
| - AuthRecaptchaAction.getOobCode: "getOobCode", |
57 |
| - AuthRecaptchaAction.signUpPassword: "signUpPassword"] |
| 75 | + private static var _shared = AuthRecaptchaVerifier() |
58 | 76 | private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
|
59 |
| - private init() {} |
| 77 | + init() {} |
60 | 78 |
|
61 | 79 | class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
|
62 | 80 | if _shared.auth != auth {
|
|
67 | 85 | return _shared
|
68 | 86 | }
|
69 | 87 |
|
| 88 | + /// This function is only for testing. |
| 89 | + class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) { |
| 90 | + _shared = instance |
| 91 | + _ = shared(auth: auth) |
| 92 | + } |
| 93 | + |
70 | 94 | func siteKey() -> String? {
|
71 | 95 | if let tenantID = auth?.tenantID {
|
72 | 96 | if let config = tenantConfigs[tenantID] {
|
|
77 | 101 | return agentConfig?.siteKey
|
78 | 102 | }
|
79 | 103 |
|
80 |
| - func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool { |
81 |
| - guard let providerString = providerToStringMap[provider] else { |
82 |
| - return false |
83 |
| - } |
84 |
| - if let tenantID = auth?.tenantID { |
85 |
| - guard let tenantConfig = tenantConfigs[tenantID], |
86 |
| - let status = tenantConfig.enablementStatus[providerString] else { |
87 |
| - return false |
88 |
| - } |
| 104 | + func enablementStatus(forProvider provider: AuthRecaptchaProvider) |
| 105 | + -> AuthRecaptchaEnablementStatus { |
| 106 | + if let tenantID = auth?.tenantID, |
| 107 | + let tenantConfig = tenantConfigs[tenantID], |
| 108 | + let status = tenantConfig.enablementStatus[provider] { |
89 | 109 | return status
|
90 |
| - } else { |
91 |
| - guard let agentConfig, |
92 |
| - let status = agentConfig.enablementStatus[providerString] else { |
93 |
| - return false |
94 |
| - } |
| 110 | + } else if let agentConfig = agentConfig, |
| 111 | + let status = agentConfig.enablementStatus[provider] { |
95 | 112 | return status
|
| 113 | + } else { |
| 114 | + return AuthRecaptchaEnablementStatus.off |
96 | 115 | }
|
97 | 116 | }
|
98 | 117 |
|
|
101 | 120 | guard let siteKey = siteKey() else {
|
102 | 121 | throw AuthErrorUtils.recaptchaSiteKeyMissing()
|
103 | 122 | }
|
104 |
| - let actionString = actionToStringMap[action] ?? "" |
| 123 | + let actionString = action.stringValue |
105 | 124 | #if !(COCOAPODS || SWIFT_PACKAGE)
|
106 | 125 | // No recaptcha on internal build system.
|
107 | 126 | return actionString
|
|
156 | 175 | let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration)
|
157 | 176 | let response = try await auth.backend.call(with: request)
|
158 | 177 | AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
|
159 |
| - // Response's site key is of the format projects/<project-id>/keys/<site-key>' |
160 |
| - guard let keys = response.recaptchaKey?.components(separatedBy: "/"), |
161 |
| - keys.count == 4 else { |
162 |
| - throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey") |
163 |
| - } |
164 |
| - let siteKey = keys[3] |
165 |
| - var enablementStatus: [String: Bool] = [:] |
| 178 | + try await parseRecaptchaConfigFromResponse(response: response) |
| 179 | + } |
| 180 | + |
| 181 | + func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws { |
| 182 | + var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:] |
| 183 | + var isRecaptchaEnabled = false |
166 | 184 | if let enforcementState = response.enforcementState {
|
167 | 185 | for state in enforcementState {
|
168 |
| - if let provider = state["provider"], |
169 |
| - provider == providerToStringMap[AuthRecaptchaProvider.password] { |
170 |
| - if let enforcement = state["enforcementState"] { |
171 |
| - if enforcement == "ENFORCE" || enforcement == "AUDIT" { |
172 |
| - enablementStatus[provider] = true |
173 |
| - } else if enforcement == "OFF" { |
174 |
| - enablementStatus[provider] = false |
175 |
| - } |
176 |
| - } |
| 186 | + guard let providerString = state["provider"], |
| 187 | + let enforcementString = state["enforcementState"], |
| 188 | + let provider = AuthRecaptchaProvider(rawValue: providerString), |
| 189 | + let enforcement = AuthRecaptchaEnablementStatus(rawValue: enforcementString) else { |
| 190 | + continue // Skip to the next state in the loop |
| 191 | + } |
| 192 | + enablementStatus[provider] = enforcement |
| 193 | + if enforcement != .off { |
| 194 | + isRecaptchaEnabled = true |
177 | 195 | }
|
178 | 196 | }
|
179 | 197 | }
|
| 198 | + var siteKey = "" |
| 199 | + // Response's site key is of the format projects/<project-id>/keys/<site-key>' |
| 200 | + if isRecaptchaEnabled { |
| 201 | + if let recaptchaKey = response.recaptchaKey { |
| 202 | + let keys = recaptchaKey.components(separatedBy: "/") |
| 203 | + if keys.count != 4 { |
| 204 | + throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey") |
| 205 | + } |
| 206 | + siteKey = keys[3] |
| 207 | + } |
| 208 | + } |
180 | 209 | let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
|
181 | 210 |
|
182 |
| - if let tenantID = auth.tenantID { |
| 211 | + if let tenantID = auth?.tenantID { |
183 | 212 | tenantConfigs[tenantID] = config
|
184 | 213 | } else {
|
185 | 214 | agentConfig = config
|
|
190 | 219 | provider: AuthRecaptchaProvider,
|
191 | 220 | action: AuthRecaptchaAction) async throws {
|
192 | 221 | try await retrieveRecaptchaConfig(forceRefresh: false)
|
193 |
| - if enablementStatus(forProvider: provider) { |
| 222 | + if enablementStatus(forProvider: provider) != .off { |
194 | 223 | let token = try await verify(forceRefresh: false, action: action)
|
195 | 224 | request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
|
196 | 225 | } else {
|
|
0 commit comments