|
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