mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-09 22:36:01 +05:45
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
275 lines
9.7 KiB
Swift
275 lines
9.7 KiB
Swift
// Copyright 2023 Google LLC
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
#if os(iOS)
|
|
|
|
import Foundation
|
|
|
|
#if SWIFT_PACKAGE
|
|
import FirebaseAuthInternal
|
|
#endif
|
|
import RecaptchaInterop
|
|
|
|
class AuthRecaptchaConfig {
|
|
var siteKey: String?
|
|
let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]
|
|
|
|
init(siteKey: String? = nil,
|
|
enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {
|
|
self.siteKey = siteKey
|
|
self.enablementStatus = enablementStatus
|
|
}
|
|
}
|
|
|
|
enum AuthRecaptchaEnablementStatus: String, CaseIterable {
|
|
case enforce = "ENFORCE"
|
|
case audit = "AUDIT"
|
|
case off = "OFF"
|
|
|
|
// Convenience property for mapping values
|
|
var stringValue: String { rawValue }
|
|
}
|
|
|
|
enum AuthRecaptchaProvider: String, CaseIterable {
|
|
case password = "EMAIL_PASSWORD_PROVIDER"
|
|
case phone = "PHONE_PROVIDER"
|
|
|
|
// Convenience property for mapping values
|
|
var stringValue: String { rawValue }
|
|
}
|
|
|
|
enum AuthRecaptchaAction: String {
|
|
case defaultAction
|
|
case signInWithPassword
|
|
case getOobCode
|
|
case signUpPassword
|
|
case sendVerificationCode
|
|
case mfaSmsSignIn
|
|
case mfaSmsEnrollment
|
|
|
|
// Convenience property for mapping values
|
|
var stringValue: String { rawValue }
|
|
}
|
|
|
|
class AuthRecaptchaVerifier {
|
|
private(set) weak var auth: Auth?
|
|
private(set) var agentConfig: AuthRecaptchaConfig?
|
|
private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
|
|
// Only initialized once. Recpatcha SDK does not support multiple clients.
|
|
private(set) var recaptchaClient: RCARecaptchaClientProtocol?
|
|
private static var _shared = AuthRecaptchaVerifier()
|
|
|
|
private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
|
|
init() {}
|
|
|
|
class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
|
|
if _shared.auth != auth {
|
|
_shared.agentConfig = nil
|
|
_shared.tenantConfigs = [:]
|
|
_shared.auth = auth
|
|
}
|
|
return _shared
|
|
}
|
|
|
|
/// This function is only for testing.
|
|
class func setShared(_ instance: AuthRecaptchaVerifier, auth: Auth?) {
|
|
_shared = instance
|
|
_ = shared(auth: auth)
|
|
}
|
|
|
|
func enablementStatus(forProvider provider: AuthRecaptchaProvider)
|
|
-> AuthRecaptchaEnablementStatus {
|
|
if let tenantID = auth?.tenantID,
|
|
let tenantConfig = tenantConfigs[tenantID],
|
|
let status = tenantConfig.enablementStatus[provider] {
|
|
return status
|
|
} else if let agentConfig = agentConfig,
|
|
let status = agentConfig.enablementStatus[provider] {
|
|
return status
|
|
} else {
|
|
return AuthRecaptchaEnablementStatus.off
|
|
}
|
|
}
|
|
|
|
func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
|
|
try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
|
|
guard let siteKey = siteKey() else {
|
|
throw AuthErrorUtils.recaptchaSiteKeyMissing()
|
|
}
|
|
let actionString = action.stringValue
|
|
#if !(COCOAPODS || SWIFT_PACKAGE)
|
|
// No recaptcha on internal build system.
|
|
return actionString
|
|
#else
|
|
|
|
let (token, error, linked, actionCreated) = await recaptchaToken(
|
|
siteKey: siteKey,
|
|
actionString: actionString,
|
|
fakeToken: "NO_RECAPTCHA"
|
|
)
|
|
|
|
guard linked else {
|
|
throw AuthErrorUtils.recaptchaSDKNotLinkedError()
|
|
}
|
|
guard actionCreated else {
|
|
throw AuthErrorUtils.recaptchaActionCreationFailed()
|
|
}
|
|
if let error {
|
|
throw error
|
|
}
|
|
if token == "NO_RECAPTCHA" {
|
|
AuthLog.logInfo(code: "I-AUT000031",
|
|
message: "reCAPTCHA token retrieval failed. NO_RECAPTCHA sent as the fake code.")
|
|
} else {
|
|
AuthLog.logInfo(
|
|
code: "I-AUT000030",
|
|
message: "reCAPTCHA token retrieval succeeded."
|
|
)
|
|
}
|
|
return token
|
|
#endif // !(COCOAPODS || SWIFT_PACKAGE)
|
|
}
|
|
|
|
func retrieveRecaptchaConfig(forceRefresh: Bool) async throws {
|
|
if !forceRefresh {
|
|
if let tenantID = auth?.tenantID {
|
|
if tenantConfigs[tenantID] != nil {
|
|
return
|
|
}
|
|
} else if agentConfig != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
guard let auth = auth else {
|
|
throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
|
|
message: "No requestConfiguration for Auth instance")
|
|
}
|
|
let request = GetRecaptchaConfigRequest(requestConfiguration: auth.requestConfiguration)
|
|
let response = try await auth.backend.call(with: request)
|
|
AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
|
|
try await parseRecaptchaConfigFromResponse(response: response)
|
|
}
|
|
|
|
private func siteKey() -> String? {
|
|
if let tenantID = auth?.tenantID {
|
|
if let config = tenantConfigs[tenantID] {
|
|
return config.siteKey
|
|
}
|
|
return nil
|
|
}
|
|
return agentConfig?.siteKey
|
|
}
|
|
|
|
func injectRecaptchaFields(request: any AuthRPCRequest,
|
|
provider: AuthRecaptchaProvider,
|
|
action: AuthRecaptchaAction) async throws {
|
|
try await retrieveRecaptchaConfig(forceRefresh: false)
|
|
if enablementStatus(forProvider: provider) != .off {
|
|
let token = try await verify(forceRefresh: false, action: action)
|
|
request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
|
|
} else {
|
|
request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
|
|
}
|
|
}
|
|
|
|
#if COCOAPODS || SWIFT_PACKAGE // No recaptcha on internal build system.
|
|
private func recaptchaToken(siteKey: String,
|
|
actionString: String,
|
|
fakeToken: String) async -> (token: String, error: Error?,
|
|
linked: Bool, actionCreated: Bool) {
|
|
if let recaptchaClient {
|
|
return await retrieveToken(
|
|
actionString: actionString,
|
|
fakeToken: fakeToken,
|
|
recaptchaClient: recaptchaClient
|
|
)
|
|
}
|
|
|
|
if let recaptcha =
|
|
NSClassFromString("RecaptchaEnterprise.RCARecaptcha") as? RCARecaptchaProtocol.Type {
|
|
do {
|
|
let client = try await recaptcha.fetchClient(withSiteKey: siteKey)
|
|
recaptchaClient = client
|
|
return await retrieveToken(
|
|
actionString: actionString,
|
|
fakeToken: fakeToken,
|
|
recaptchaClient: client
|
|
)
|
|
} catch {
|
|
return ("", error, true, true)
|
|
}
|
|
} else {
|
|
// RecaptchaEnterprise not linked.
|
|
return ("", nil, false, false)
|
|
}
|
|
}
|
|
#endif // (COCOAPODS || SWIFT_PACKAGE)
|
|
|
|
private func retrieveToken(actionString: String,
|
|
fakeToken: String,
|
|
recaptchaClient: RCARecaptchaClientProtocol) async -> (token: String,
|
|
error: Error?,
|
|
linked: Bool,
|
|
actionCreated: Bool) {
|
|
if let recaptchaAction =
|
|
NSClassFromString("RecaptchaEnterprise.RCAAction") as? RCAActionProtocol.Type {
|
|
let action = recaptchaAction.init(customAction: actionString)
|
|
let token = try? await recaptchaClient.execute(withAction: action)
|
|
return (token ?? "NO_RECAPTCHA", nil, true, true)
|
|
} else {
|
|
// RecaptchaEnterprise not linked.
|
|
return ("", nil, false, false)
|
|
}
|
|
}
|
|
|
|
private func parseRecaptchaConfigFromResponse(response: GetRecaptchaConfigResponse) async throws {
|
|
var enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus] = [:]
|
|
var isRecaptchaEnabled = false
|
|
if let enforcementState = response.enforcementState {
|
|
for state in enforcementState {
|
|
guard let providerString = state["provider"],
|
|
let enforcementString = state["enforcementState"],
|
|
let provider = AuthRecaptchaProvider(rawValue: providerString),
|
|
let enforcement = AuthRecaptchaEnablementStatus(rawValue: enforcementString) else {
|
|
continue // Skip to the next state in the loop
|
|
}
|
|
enablementStatus[provider] = enforcement
|
|
if enforcement != .off {
|
|
isRecaptchaEnabled = true
|
|
}
|
|
}
|
|
}
|
|
var siteKey = ""
|
|
// Response's site key is of the format projects/<project-id>/keys/<site-key>'
|
|
if isRecaptchaEnabled {
|
|
if let recaptchaKey = response.recaptchaKey {
|
|
let keys = recaptchaKey.components(separatedBy: "/")
|
|
if keys.count != 4 {
|
|
throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
|
|
}
|
|
siteKey = keys[3]
|
|
}
|
|
}
|
|
let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
|
|
|
|
if let tenantID = auth?.tenantID {
|
|
tenantConfigs[tenantID] = config
|
|
} else {
|
|
agentConfig = config
|
|
}
|
|
}
|
|
}
|
|
#endif
|