mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
202 lines
7.3 KiB
Swift
202 lines
7.3 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
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
class AuthRecaptchaConfig {
|
|
let siteKey: String
|
|
let enablementStatus: [String: Bool]
|
|
|
|
init(siteKey: String, enablementStatus: [String: Bool]) {
|
|
self.siteKey = siteKey
|
|
self.enablementStatus = enablementStatus
|
|
}
|
|
}
|
|
|
|
enum AuthRecaptchaProvider {
|
|
case password
|
|
}
|
|
|
|
enum AuthRecaptchaAction {
|
|
case defaultAction
|
|
case signInWithPassword
|
|
case getOobCode
|
|
case signUpPassword
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
class AuthRecaptchaVerifier {
|
|
private(set) weak var auth: Auth?
|
|
private(set) var agentConfig: AuthRecaptchaConfig?
|
|
private(set) var tenantConfigs: [String: AuthRecaptchaConfig] = [:]
|
|
private(set) var recaptchaClient: RCARecaptchaClientProtocol?
|
|
|
|
private static let _shared = AuthRecaptchaVerifier()
|
|
private let providerToStringMap = [AuthRecaptchaProvider.password: "EMAIL_PASSWORD_PROVIDER"]
|
|
private let actionToStringMap = [AuthRecaptchaAction.signInWithPassword: "signInWithPassword",
|
|
AuthRecaptchaAction.getOobCode: "getOobCode",
|
|
AuthRecaptchaAction.signUpPassword: "signUpPassword"]
|
|
private let kRecaptchaVersion = "RECAPTCHA_ENTERPRISE"
|
|
private init() {}
|
|
|
|
class func shared(auth: Auth?) -> AuthRecaptchaVerifier {
|
|
if _shared.auth != auth {
|
|
_shared.agentConfig = nil
|
|
_shared.tenantConfigs = [:]
|
|
_shared.auth = auth
|
|
}
|
|
return _shared
|
|
}
|
|
|
|
func siteKey() -> String? {
|
|
if let tenantID = auth?.tenantID {
|
|
if let config = tenantConfigs[tenantID] {
|
|
return config.siteKey
|
|
}
|
|
return nil
|
|
}
|
|
return agentConfig?.siteKey
|
|
}
|
|
|
|
func enablementStatus(forProvider provider: AuthRecaptchaProvider) -> Bool {
|
|
guard let providerString = providerToStringMap[provider] else {
|
|
return false
|
|
}
|
|
if let tenantID = auth?.tenantID {
|
|
guard let tenantConfig = tenantConfigs[tenantID],
|
|
let status = tenantConfig.enablementStatus[providerString] else {
|
|
return false
|
|
}
|
|
return status
|
|
} else {
|
|
guard let agentConfig,
|
|
let status = agentConfig.enablementStatus[providerString] else {
|
|
return false
|
|
}
|
|
return status
|
|
}
|
|
}
|
|
|
|
func verify(forceRefresh: Bool, action: AuthRecaptchaAction) async throws -> String {
|
|
try await retrieveRecaptchaConfig(forceRefresh: forceRefresh)
|
|
guard let siteKey = siteKey() else {
|
|
throw AuthErrorUtils.recaptchaSiteKeyMissing()
|
|
}
|
|
let actionString = actionToStringMap[action] ?? ""
|
|
#if !(COCOAPODS || SWIFT_PACKAGE)
|
|
// No recaptcha on internal build system.
|
|
return actionString
|
|
#else
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
FIRRecaptchaGetToken(siteKey, actionString,
|
|
"NO_RECAPTCHA") { (token: String, error: Error?,
|
|
linked: Bool, actionCreated: Bool) in
|
|
guard linked else {
|
|
continuation.resume(throwing: AuthErrorUtils.recaptchaSDKNotLinkedError())
|
|
return
|
|
}
|
|
guard actionCreated else {
|
|
continuation.resume(throwing: AuthErrorUtils.recaptchaActionCreationFailed())
|
|
return
|
|
}
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
return
|
|
} else {
|
|
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."
|
|
)
|
|
}
|
|
continuation.resume(returning: 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 requestConfiguration = auth?.requestConfiguration else {
|
|
throw AuthErrorUtils.error(code: .recaptchaNotEnabled,
|
|
message: "No requestConfiguration for Auth instance")
|
|
}
|
|
let request = GetRecaptchaConfigRequest(requestConfiguration: requestConfiguration)
|
|
let response = try await AuthBackend.call(with: request)
|
|
AuthLog.logInfo(code: "I-AUT000029", message: "reCAPTCHA config retrieval succeeded.")
|
|
// Response's site key is of the format projects/<project-id>/keys/<site-key>'
|
|
guard let keys = response.recaptchaKey?.components(separatedBy: "/"),
|
|
keys.count == 4 else {
|
|
throw AuthErrorUtils.error(code: .recaptchaNotEnabled, message: "Invalid siteKey")
|
|
}
|
|
let siteKey = keys[3]
|
|
var enablementStatus: [String: Bool] = [:]
|
|
if let enforcementState = response.enforcementState {
|
|
for state in enforcementState {
|
|
if let provider = state["provider"],
|
|
provider == providerToStringMap[AuthRecaptchaProvider.password] {
|
|
if let enforcement = state["enforcementState"] {
|
|
if enforcement == "ENFORCE" || enforcement == "AUDIT" {
|
|
enablementStatus[provider] = true
|
|
} else if enforcement == "OFF" {
|
|
enablementStatus[provider] = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let config = AuthRecaptchaConfig(siteKey: siteKey, enablementStatus: enablementStatus)
|
|
|
|
if let tenantID = auth?.tenantID {
|
|
tenantConfigs[tenantID] = config
|
|
} else {
|
|
agentConfig = config
|
|
}
|
|
}
|
|
|
|
func injectRecaptchaFields(request: any AuthRPCRequest,
|
|
provider: AuthRecaptchaProvider,
|
|
action: AuthRecaptchaAction) async throws {
|
|
try await retrieveRecaptchaConfig(forceRefresh: false)
|
|
if enablementStatus(forProvider: provider) {
|
|
let token = try await verify(forceRefresh: false, action: action)
|
|
request.injectRecaptchaFields(recaptchaResponse: token, recaptchaVersion: kRecaptchaVersion)
|
|
} else {
|
|
request.injectRecaptchaFields(recaptchaResponse: nil, recaptchaVersion: kRecaptchaVersion)
|
|
}
|
|
}
|
|
}
|
|
#endif
|