mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-10 22:45:59 +05:45
231 lines
8.3 KiB
Swift
231 lines
8.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 {
|
|
var siteKey: String?
|
|
let enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]
|
|
|
|
init(siteKey: String? = nil,
|
|
enablementStatus: [AuthRecaptchaProvider: AuthRecaptchaEnablementStatus]) {
|
|
self.siteKey = siteKey
|
|
self.enablementStatus = enablementStatus
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
enum AuthRecaptchaEnablementStatus: String, CaseIterable {
|
|
case enforce = "ENFORCE"
|
|
case audit = "AUDIT"
|
|
case off = "OFF"
|
|
|
|
// Convenience property for mapping values
|
|
var stringValue: String { rawValue }
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
enum AuthRecaptchaProvider: String, CaseIterable {
|
|
case password = "EMAIL_PASSWORD_PROVIDER"
|
|
case phone = "PHONE_PROVIDER"
|
|
|
|
// Convenience property for mapping values
|
|
var stringValue: String { rawValue }
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
|
|
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 }
|
|
}
|
|
|
|
@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 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 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)
|
|
-> 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
|
|
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 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)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
#endif
|