Files
firebase-ios-sdk/FirebaseAuth/Sources/Swift/AuthProvider/PhoneAuthProvider.swift
T
Nick Cookeandgemini-code-assist[bot] dc18b7f5b9 refactor(global): cleanup redundant availability checks and update sample apps audit (#16143)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
2026-04-30 18:48:30 -04:00

666 lines
32 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.
import FirebaseCore
import Foundation
// TODO(Swift 6 Breaking): Make checked Sendable.
/// A concrete implementation of `AuthProvider` for phone auth providers.
///
/// This class is available on iOS only.
@objc(FIRPhoneAuthProvider) open class PhoneAuthProvider: NSObject, @unchecked Sendable {
/// A string constant identifying the phone identity provider.
@objc public static let id = "phone"
private static let recaptchaVersion = "RECAPTCHA_ENTERPRISE"
private static let clientType = "CLIENT_TYPE_IOS"
private static let fakeCaptchaResponse = "NO_RECAPTCHA"
#if os(iOS)
/// Returns an instance of `PhoneAuthProvider` for the default `Auth` object.
@objc(provider) open class func provider() -> PhoneAuthProvider {
return PhoneAuthProvider(auth: Auth.auth())
}
/// Returns an instance of `PhoneAuthProvider` for the provided `Auth` object.
/// - Parameter auth: The auth object to associate with the phone auth provider instance.
@objc(providerWithAuth:)
open class func provider(auth: Auth) -> PhoneAuthProvider {
return PhoneAuthProvider(auth: auth)
}
/// Starts the phone number authentication flow by sending a verification code to the
/// specified phone number.
///
/// Possible error codes:
/// * `AuthErrorCodeCaptchaCheckFailed` - Indicates that the reCAPTCHA token obtained by
/// the Firebase Auth is invalid or has expired.
/// * `AuthErrorCodeQuotaExceeded` - Indicates that the phone verification quota for this
/// project has been exceeded.
/// * `AuthErrorCodeInvalidPhoneNumber` - Indicates that the phone number provided is invalid.
/// * `AuthErrorCodeMissingPhoneNumber` - Indicates that a phone number was not provided.
/// - Parameter phoneNumber: The phone number to be verified.
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
/// retained by this method until the completion block is executed.
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
@objc(verifyPhoneNumber:UIDelegate:completion:)
open func verifyPhoneNumber(_ phoneNumber: String,
uiDelegate: AuthUIDelegate? = nil,
completion: (@MainActor (String?, Error?) -> Void)?) {
verifyPhoneNumber(phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: nil,
completion: completion)
}
/// Verify ownership of the second factor phone number by the current user.
/// - Parameter phoneNumber: The phone number to be verified.
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
/// retained by this method until the completion block is executed.
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
/// passed the first factor challenge.
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
@objc(verifyPhoneNumber:UIDelegate:multiFactorSession:completion:)
open func verifyPhoneNumber(_ phoneNumber: String,
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession? = nil,
completion: (@MainActor (String?, Error?) -> Void)?) {
Task {
do {
let verificationID = try await verifyPhoneNumber(
phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession
)
await completion?(verificationID, nil)
} catch {
await completion?(nil, error)
}
}
}
/// Verify ownership of the second factor phone number by the current user.
/// - Parameter phoneNumber: The phone number to be verified.
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
/// retained by this method until the completion block is executed.
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
/// passed the first factor challenge.
/// - Returns: The verification ID
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
open func verifyPhoneNumber(_ phoneNumber: String,
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession? = nil) async throws
-> String {
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
urlTypes: auth.mainBundleUrlTypes) else {
fatalError(
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
)
}
if let verificationID = try await internalVerify(phoneNumber: phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession) {
return verificationID
} else {
throw AuthErrorUtils.invalidVerificationIDError(message: "Invalid verification ID")
}
}
/// Verify ownership of the second factor phone number by the current user.
/// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified.
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
/// retained by this method until the completion block is executed.
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
/// passed the first factor challenge.
/// - Parameter completion: The callback to be invoked when the verification flow is finished.
@objc(verifyPhoneNumberWithMultiFactorInfo:UIDelegate:multiFactorSession:completion:)
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession?,
completion: ((String?, Error?) -> Void)?) {
Task {
do {
let verificationID = try await verifyPhoneNumber(
with: multiFactorInfo,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession
)
await MainActor.run {
completion?(verificationID, nil)
}
} catch {
await MainActor.run {
completion?(nil, error)
}
}
}
}
/// Verify ownership of the second factor phone number by the current user.
/// - Parameter multiFactorInfo: The phone multi factor whose number need to be verified.
/// - Parameter uiDelegate: An object used to present the SFSafariViewController. The object is
/// retained by this method until the completion block is executed.
/// - Parameter multiFactorSession: A session to identify the MFA flow. For enrollment, this
/// identifies the user trying to enroll. For sign-in, this identifies that the user already
/// passed the first factor challenge.
/// - Returns: The verification ID.
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
open func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
uiDelegate: AuthUIDelegate? = nil,
multiFactorSession: MultiFactorSession?) async throws -> String {
multiFactorSession?.multiFactorInfo = multiFactorInfo
return try await verifyPhoneNumber(multiFactorInfo.phoneNumber,
uiDelegate: uiDelegate,
multiFactorSession: multiFactorSession)
}
/// Creates an `AuthCredential` for the phone number provider identified by the
/// verification ID and verification code.
///
/// - Parameter verificationID: The verification ID obtained from invoking
/// verifyPhoneNumber:completion:
/// - Parameter verificationCode: The verification code obtained from the user.
/// - Returns: The corresponding phone auth credential for the verification ID and verification
/// code provided.
@objc(credentialWithVerificationID:verificationCode:)
open func credential(withVerificationID verificationID: String,
verificationCode: String) -> PhoneAuthCredential {
return PhoneAuthCredential(withProviderID: PhoneAuthProvider.id,
verificationID: verificationID,
verificationCode: verificationCode)
}
private func internalVerify(phoneNumber: String,
uiDelegate: AuthUIDelegate?,
multiFactorSession: MultiFactorSession? = nil) async throws
-> String? {
guard !phoneNumber.isEmpty else {
throw AuthErrorUtils.missingPhoneNumberError(message: nil)
}
guard let manager = auth.notificationManager else {
throw AuthErrorUtils.notificationNotForwardedError()
}
guard await manager.checkNotificationForwarding() else {
throw AuthErrorUtils.notificationNotForwardedError()
}
let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: auth)
if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
// If app verification is disabled for testing
// do not fetch recaptcha config, as this is not implemented in emulator
// Treat this same as RCE enable status off
return try await verifyClAndSendVerificationCode(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate
)
}
try await recaptchaVerifier.retrieveRecaptchaConfig(forceRefresh: true)
switch recaptchaVerifier.enablementStatus(forProvider: .phone) {
case .off:
return try await verifyClAndSendVerificationCode(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate
)
case .audit:
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: true,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
case .enforce:
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: false,
multiFactorSession: multiFactorSession,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
}
}
func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
-> String? {
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth
.requestConfiguration)
do {
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .sendVerificationCode
)
let response = try await auth.backend.call(with: request)
return response.verificationID
} catch {
return try await handleVerifyErrorWithRetry(error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate,
auditFallback: true)
}
}
/// Starts the flow to verify the client via silent push notification.
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
/// - Parameter phoneNumber: The phone number to be verified.
/// - Parameter callback: The callback to be invoked on the global work queue when the flow is
/// finished.
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
uiDelegate: AuthUIDelegate?,
auditFallback: Bool = false) async throws
-> String? {
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
let request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
codeIdentity: codeIdentity,
requestConfiguration: auth
.requestConfiguration)
if auditFallback {
request.injectRecaptchaFields(
recaptchaResponse: PhoneAuthProvider.fakeCaptchaResponse,
recaptchaVersion: PhoneAuthProvider.recaptchaVersion
)
}
do {
let response = try await auth.backend.call(with: request)
return response.verificationID
} catch {
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: nil,
uiDelegate: uiDelegate,
auditFallback: auditFallback
)
}
}
/// Starts the flow to verify the client via silent push notification. This is used in both
/// .Audit and .Enforce mode
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
/// - Parameter phoneNumber: The phone number to be verified.
private func verifyClAndSendVerificationCodeWithRecaptcha(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?,
recaptchaVerifier: AuthRecaptchaVerifier) async throws
-> String? {
if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
let request = SendVerificationCodeRequest(
phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth.requestConfiguration
)
let response = try await auth.backend.call(with: request)
return response.verificationID
}
guard let session else {
return try await verifyClAndSendVerificationCodeWithRecaptcha(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
uiDelegate: uiDelegate,
recaptchaVerifier: recaptchaVerifier
)
}
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty)
do {
if let idToken = session.idToken {
let request = StartMFAEnrollmentRequest(idToken: idToken,
enrollmentInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .mfaSmsEnrollment
)
let response = try await auth.backend.call(with: request)
return response.phoneSessionInfo?.sessionInfo
} else {
let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
MFAEnrollmentID: session.multiFactorInfo?.uid,
signInInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
try await recaptchaVerifier.injectRecaptchaFields(
request: request,
provider: .phone,
action: .mfaSmsSignIn
)
let response = try await auth.backend.call(with: request)
return response.responseInfo.sessionInfo
}
} catch {
// For Audit fallback only after rCE check failed
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: session,
uiDelegate: uiDelegate,
auditFallback: true
)
}
}
/// Starts the flow to verify the client via silent push notification.
/// This method is called in Audit fallback flow with "NO_RECAPTCHA" fake token and Off flow
/// - Parameter retryOnInvalidAppCredential: Whether or not the flow should be retried if an
/// AuthErrorCodeInvalidAppCredential error is returned from the backend.
/// - Parameter phoneNumber: The phone number to be verified.
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?,
auditFallback: Bool = false) async throws
-> String? {
if let settings = auth.settings,
settings.isAppVerificationDisabledForTesting {
let request = SendVerificationCodeRequest(
phoneNumber: phoneNumber,
codeIdentity: CodeIdentity.empty,
requestConfiguration: auth.requestConfiguration
)
let response = try await auth.backend.call(with: request)
return response.verificationID
}
guard let session else {
// Phone MFA flow
return try await verifyClAndSendVerificationCode(
toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
uiDelegate: uiDelegate,
auditFallback: auditFallback
)
}
// MFA flows
let codeIdentity = try await verifyClient(withUIDelegate: uiDelegate)
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
codeIdentity: codeIdentity)
if auditFallback {
startMFARequestInfo.injectRecaptchaFields(
recaptchaResponse: PhoneAuthProvider.fakeCaptchaResponse,
recaptchaVersion: PhoneAuthProvider.recaptchaVersion,
clientType: PhoneAuthProvider.clientType
)
}
do {
if let idToken = session.idToken {
let request = StartMFAEnrollmentRequest(idToken: idToken,
enrollmentInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
let response = try await auth.backend.call(with: request)
return response.phoneSessionInfo?.sessionInfo
} else {
let request = StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
MFAEnrollmentID: session.multiFactorInfo?.uid,
signInInfo: startMFARequestInfo,
requestConfiguration: auth.requestConfiguration)
let response = try await auth.backend.call(with: request)
return response.responseInfo.sessionInfo
}
} catch {
return try await handleVerifyErrorWithRetry(
error: error,
phoneNumber: phoneNumber,
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
multiFactorSession: session,
uiDelegate: uiDelegate,
auditFallback: auditFallback
)
}
}
/// This method is only called when Audit failed on rCE on invalid-app-credential exception
private func handleVerifyErrorWithRetry(error: Error,
phoneNumber: String,
retryOnInvalidAppCredential: Bool,
multiFactorSession session: MultiFactorSession?,
uiDelegate: AuthUIDelegate?,
auditFallback: Bool = false) async throws -> String? {
if (error as NSError).code == AuthErrorCode.invalidAppCredential.rawValue {
if retryOnInvalidAppCredential {
auth.appCredentialManager.clearCredential()
return try await verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
retryOnInvalidAppCredential: false,
multiFactorSession: session,
uiDelegate: uiDelegate,
auditFallback: auditFallback)
}
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: nil, underlyingError: error)
}
throw error
}
/// Continues the flow to verify the client via silent push notification.
private func verifyClient(withUIDelegate uiDelegate: AuthUIDelegate?) async throws
-> CodeIdentity {
// Remove the simulator check below after FCM supports APNs in simulators
#if targetEnvironment(simulator)
let environment = ProcessInfo().environment
if environment["XCTestConfigurationFilePath"] == nil {
return try await CodeIdentity
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
}
#endif
if let credential = auth.appCredentialManager.credential {
return CodeIdentity.credential(credential)
}
var token: AuthAPNSToken
do {
token = try await auth.tokenManager.getToken()
} catch {
return try await CodeIdentity
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
}
let request = VerifyClientRequest(withAppToken: token.string,
isSandbox: token.type == AuthAPNSTokenType.sandbox,
requestConfiguration: auth.requestConfiguration)
do {
let verifyResponse = try await auth.backend.call(with: request)
guard let receipt = verifyResponse.receipt,
let timeout = verifyResponse.suggestedTimeOutDate?.timeIntervalSinceNow else {
fatalError("Internal Auth Error: invalid VerifyClientResponse")
}
let credential = await
auth.appCredentialManager.didStartVerification(withReceipt: receipt, timeout: timeout)
if credential.secret == nil {
AuthLog.logWarning(code: "I-AUT000014", message: "Failed to receive remote " +
"notification to verify app identity within \(timeout) " +
"second(s), falling back to reCAPTCHA verification.")
return try await CodeIdentity
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
}
return CodeIdentity.credential(credential)
} catch {
let nserror = error as NSError
// reCAPTCHA Flow if it's an invalid app credential or a missing app token.
guard nserror.code == AuthErrorCode.invalidAppCredential.rawValue || nserror
.code == AuthErrorCode.missingAppToken.rawValue else {
throw error
}
return try await CodeIdentity
.recaptcha(reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate))
}
}
/// Continues the flow to verify the client via silent push notification.
private func reCAPTCHAFlowWithUIDelegate(withUIDelegate uiDelegate: AuthUIDelegate?) async throws
-> String {
let eventID = AuthWebUtils.randomString(withLength: 10)
guard let url = try await reCAPTCHAURL(withEventID: eventID) else {
fatalError(
"Internal error: reCAPTCHAURL returned neither a value nor an error. Report issue"
)
}
let callbackMatcher: (URL?) -> Bool = { callbackURL in
AuthWebUtils.isExpectedCallbackURL(
callbackURL,
eventID: eventID,
authType: self.kAuthTypeVerifyApp,
callbackScheme: self.callbackScheme
)
}
return try await withUnsafeThrowingContinuation { continuation in
self.auth.authURLPresenter.present(url,
uiDelegate: uiDelegate,
callbackMatcher: callbackMatcher) { callbackURL, error in
if let error {
continuation.resume(throwing: error)
} else {
do {
try continuation.resume(returning: self.reCAPTCHAToken(forURL: callbackURL))
} catch {
continuation.resume(throwing: error)
}
}
}
}
}
/// Parses the reCAPTCHA URL and returns the reCAPTCHA token.
/// - Parameter url: The url to be parsed for a reCAPTCHA token.
/// - Returns: The reCAPTCHA token if successful.
private func reCAPTCHAToken(forURL url: URL?) throws -> String {
guard let url = url else {
let reason = "Internal Auth Error: nil URL trying to access RECAPTCHA token"
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
}
let actualURLComponents = URLComponents(url: url, resolvingAgainstBaseURL: false)
if let queryItems = actualURLComponents?.queryItems,
let deepLinkURL = AuthWebUtils.queryItemValue(name: "deep_link_id", from: queryItems) {
let deepLinkComponents = URLComponents(string: deepLinkURL)
if let queryItems = deepLinkComponents?.queryItems {
if let token = AuthWebUtils.queryItemValue(name: "recaptchaToken", from: queryItems) {
return token
}
if let firebaseError = AuthWebUtils.queryItemValue(
name: "firebaseError",
from: queryItems
) {
if let errorData = firebaseError.data(using: .utf8) {
var errorDict: [AnyHashable: Any]?
do {
errorDict = try JSONSerialization.jsonObject(with: errorData) as? [AnyHashable: Any]
} catch {
throw AuthErrorUtils.JSONSerializationError(underlyingError: error)
}
if let errorDict,
let code = errorDict["code"] as? String,
let message = errorDict["message"] as? String {
throw AuthErrorUtils.urlResponseError(code: code, message: message)
}
}
}
}
let reason = "An unknown error occurred with the following response: \(deepLinkURL)"
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
}
let reason = "Failed to get url Components for url: \(url)"
throw AuthErrorUtils.appVerificationUserInteractionFailure(reason: reason)
}
/// Constructs a URL used for opening a reCAPTCHA app verification flow using a given event ID.
/// - Parameter eventID: The event ID used for this purpose.
private func reCAPTCHAURL(withEventID eventID: String) async throws -> URL? {
let authDomain = try await AuthWebUtils
.fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration, backend: auth.backend)
let bundleID = Bundle.main.bundleIdentifier
let clientID = auth.app?.options.clientID
let appID = auth.app?.options.googleAppID
let apiKey = auth.requestConfiguration.apiKey
let appCheck = auth.requestConfiguration.appCheck
var queryItems = [URLQueryItem(name: "apiKey", value: apiKey),
URLQueryItem(name: "authType", value: kAuthTypeVerifyApp),
URLQueryItem(name: "ibi", value: bundleID ?? ""),
URLQueryItem(name: "v", value: AuthBackend.authUserAgent()),
URLQueryItem(name: "eventId", value: eventID)]
if usingClientIDScheme {
queryItems.append(URLQueryItem(name: "clientId", value: clientID))
} else {
queryItems.append(URLQueryItem(name: "appId", value: appID))
}
if let languageCode = auth.requestConfiguration.languageCode {
queryItems.append(URLQueryItem(name: "hl", value: languageCode))
}
var components = URLComponents(string: "https://\(authDomain)/__/auth/handler?")
components?.queryItems = queryItems
if let appCheck {
let tokenResult = await appCheck.getToken(forcingRefresh: false)
if let error = tokenResult.error {
AuthLog.logWarning(code: "I-AUT000018",
message: "Error getting App Check token; using placeholder " +
"token instead. Error: \(error)")
}
let appCheckTokenFragment = "fac=\(tokenResult.token)"
components?.fragment = appCheckTokenFragment
}
return components?.url
}
private let auth: Auth
private let callbackScheme: String
private let usingClientIDScheme: Bool
init(auth: Auth) {
self.auth = auth
if let clientID = auth.app?.options.clientID {
let reverseClientIDScheme = clientID.components(separatedBy: ".").reversed()
.joined(separator: ".")
if AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: reverseClientIDScheme,
urlTypes: auth.mainBundleUrlTypes) {
callbackScheme = reverseClientIDScheme
usingClientIDScheme = true
return
}
}
usingClientIDScheme = false
if let appID = auth.app?.options.googleAppID {
let dashedAppID = appID.replacingOccurrences(of: ":", with: "-")
callbackScheme = "app-\(dashedAppID)"
return
}
callbackScheme = ""
}
private let kAuthTypeVerifyApp = "verifyApp"
#endif
}