mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-11 22:55:59 +05:45
611 lines
28 KiB
Swift
611 lines
28 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 Foundation
|
|
@_implementationOnly import FirebaseCore
|
|
|
|
// When building for CocoaPods, non-public headers are exposed to Swift via a
|
|
// private module map.
|
|
#if COCOAPODS
|
|
@_implementationOnly import FirebaseAuth_Private
|
|
#endif // COCOAPODS
|
|
|
|
/**
|
|
@brief A concrete implementation of `AuthProvider` for phone auth providers.
|
|
This class is available on iOS only.
|
|
*/
|
|
@objc(FIRPhoneAuthProvider) open class PhoneAuthProvider: NSObject {
|
|
@objc public static let id = "phone"
|
|
#if os(iOS)
|
|
/**
|
|
@brief Returns an instance of `PhoneAuthProvider` for the default `Auth` object.
|
|
*/
|
|
@objc(provider) public class func provider() -> PhoneAuthProvider {
|
|
return PhoneAuthProvider(auth: Auth.auth())
|
|
}
|
|
|
|
/**
|
|
@brief Returns an instance of `PhoneAuthProvider` for the provided `Auth` object.
|
|
@param auth The auth object to associate with the phone auth provider instance.
|
|
*/
|
|
@objc(providerWithAuth:)
|
|
public class func provider(auth: Auth) -> PhoneAuthProvider {
|
|
return PhoneAuthProvider(auth: auth)
|
|
}
|
|
|
|
// TODO: review/remove public objc
|
|
|
|
/**
|
|
@brief Starts the phone number authentication flow by sending a verification code to the
|
|
specified phone number.
|
|
@param phoneNumber The phone number to be verified.
|
|
@param uiDelegate An object used to present the SFSafariViewController. The object is retained
|
|
by this method until the completion block is executed.
|
|
@param completion The callback to be invoked when the verification flow is finished.
|
|
@remarks 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.
|
|
*/
|
|
@objc(verifyPhoneNumber:UIDelegate:completion:)
|
|
public func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate?,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
|
|
urlTypes: auth.mainBundleUrlTypes) else {
|
|
fatalError(
|
|
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
|
|
)
|
|
}
|
|
Auth.globalWorkQueue().async {
|
|
let callbackOnMainThread: (String?, Error?) -> Void = { verificationID, error in
|
|
if let completion {
|
|
DispatchQueue.main.async {
|
|
completion(verificationID, error)
|
|
}
|
|
}
|
|
}
|
|
self.internalVerify(phoneNumber: phoneNumber,
|
|
uiDelegate: uiDelegate) { verificationID, error in
|
|
if let error {
|
|
callbackOnMainThread(nil, error)
|
|
} else {
|
|
callbackOnMainThread(verificationID, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Verify ownership of the second factor phone number by the current user.
|
|
@param phoneNumber The phone number to be verified.
|
|
@param uiDelegate An object used to present the SFSafariViewController. The object is retained
|
|
by this method until the completion block is executed.
|
|
@param 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.
|
|
@param completion The callback to be invoked when the verification flow is finished.
|
|
*/
|
|
|
|
@objc(verifyPhoneNumber:UIDelegate:multiFactorSession:completion:)
|
|
public func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate?,
|
|
multiFactorSession session: MultiFactorSession? = nil,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
guard let session else {
|
|
verifyPhoneNumber(phoneNumber, uiDelegate: uiDelegate, completion: completion)
|
|
return
|
|
}
|
|
guard AuthWebUtils.isCallbackSchemeRegistered(forCustomURLScheme: callbackScheme,
|
|
urlTypes: auth.mainBundleUrlTypes) else {
|
|
fatalError(
|
|
"Please register custom URL scheme \(callbackScheme) in the app's Info.plist file."
|
|
)
|
|
}
|
|
Auth.globalWorkQueue().async {
|
|
let callbackOnMainThread: (String?, Error?) -> Void = { verificationID, error in
|
|
if let completion {
|
|
DispatchQueue.main.async {
|
|
completion(verificationID, error)
|
|
}
|
|
}
|
|
}
|
|
self.internalVerify(phoneNumber: phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: session) { verificationID, error in
|
|
if let error {
|
|
callbackOnMainThread(nil, error)
|
|
} else {
|
|
callbackOnMainThread(verificationID, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
|
|
public func verifyPhoneNumber(_ phoneNumber: String,
|
|
uiDelegate: AuthUIDelegate?,
|
|
multiFactorSession: MultiFactorSession? = nil) async throws
|
|
-> String {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.verifyPhoneNumber(phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession) { result, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let result {
|
|
continuation.resume(returning: result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Verify ownership of the second factor phone number by the current user.
|
|
@param multiFactorInfo The phone multi factor whose number need to be verified.
|
|
@param uiDelegate An object used to present the SFSafariViewController. The object is retained
|
|
by this method until the completion block is executed.
|
|
@param 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.
|
|
@param completion The callback to be invoked when the verification flow is finished.
|
|
*/
|
|
@objc(verifyPhoneNumberWithMultiFactorInfo:UIDelegate:multiFactorSession:completion:)
|
|
public func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
|
|
uiDelegate: AuthUIDelegate?,
|
|
multiFactorSession session: MultiFactorSession?,
|
|
completion: ((_: String?, _: Error?) -> Void)?) {
|
|
session?.multiFactorInfo = multiFactorInfo
|
|
verifyPhoneNumber(multiFactorInfo.phoneNumber,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: session,
|
|
completion: completion)
|
|
}
|
|
|
|
@available(iOS 13, tvOS 13, macOS 10.15, watchOS 8, *)
|
|
public func verifyPhoneNumber(with multiFactorInfo: PhoneMultiFactorInfo,
|
|
uiDelegate: AuthUIDelegate?,
|
|
multiFactorSession: MultiFactorSession?) async throws -> String {
|
|
return try await withCheckedThrowingContinuation { continuation in
|
|
self.verifyPhoneNumber(with: multiFactorInfo,
|
|
uiDelegate: uiDelegate,
|
|
multiFactorSession: multiFactorSession) { result, error in
|
|
if let error {
|
|
continuation.resume(throwing: error)
|
|
} else if let result {
|
|
continuation.resume(returning: result)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Creates an `AuthCredential` for the phone number provider identified by the
|
|
verification ID and verification code.
|
|
|
|
@param verificationID The verification ID obtained from invoking
|
|
verifyPhoneNumber:completion:
|
|
@param verificationCode The verification code obtained from the user.
|
|
@return The corresponding phone auth credential for the verification ID and verification code
|
|
provided.
|
|
*/
|
|
@objc(credentialWithVerificationID:verificationCode:)
|
|
public 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 session: MultiFactorSession? = nil,
|
|
completion: @escaping ((String?, Error?) -> Void)) {
|
|
guard phoneNumber.count > 0 else {
|
|
completion(nil, AuthErrorUtils.missingPhoneNumberError(message: nil))
|
|
return
|
|
}
|
|
auth.notificationManager.checkNotificationForwarding { isNotificationBeingForwarded in
|
|
guard isNotificationBeingForwarded else {
|
|
completion(nil, AuthErrorUtils.notificationNotForwardedError())
|
|
return
|
|
}
|
|
self.verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: true,
|
|
multiFactorSession: session,
|
|
uiDelegate: uiDelegate) { verificationID, error in
|
|
completion(verificationID, error)
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @fn
|
|
@brief Starts the flow to verify the client via silent push notification.
|
|
@param retryOnInvalidAppCredential Whether of not the flow should be retried if an
|
|
AuthErrorCodeInvalidAppCredential error is returned from the backend.
|
|
@param phoneNumber The phone number to be verified.
|
|
@param 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?,
|
|
callback: @escaping (String?, Error?) -> Void) {
|
|
verifyClient(withUIDelegate: uiDelegate) { appCredential, reCAPTCHAToken, error in
|
|
if let error {
|
|
callback(nil, error)
|
|
return
|
|
}
|
|
var request: SendVerificationCodeRequest?
|
|
if let appCredential {
|
|
request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
|
|
appCredential: appCredential,
|
|
reCAPTCHAToken: nil,
|
|
requestConfiguration: self.auth
|
|
.requestConfiguration)
|
|
} else if let reCAPTCHAToken {
|
|
request = SendVerificationCodeRequest(phoneNumber: phoneNumber,
|
|
appCredential: nil,
|
|
reCAPTCHAToken: reCAPTCHAToken,
|
|
requestConfiguration: self.auth
|
|
.requestConfiguration)
|
|
} else {
|
|
fatalError("Internal Phone Auth Error:Both reCAPTCHA token and app credential are nil")
|
|
}
|
|
if let request {
|
|
AuthBackend.post(withRequest: request) { response, error in
|
|
if let error {
|
|
self.handleVerifyErrorWithRetry(error: error,
|
|
phoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
multiFactorSession: nil,
|
|
uiDelegate: uiDelegate,
|
|
callback: callback)
|
|
return
|
|
}
|
|
callback((response as? SendVerificationCodeResponse)?.verificationID, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @fn
|
|
@brief Starts the flow to verify the client via silent push notification.
|
|
@param retryOnInvalidAppCredential Whether of not the flow should be retried if an
|
|
AuthErrorCodeInvalidAppCredential error is returned from the backend.
|
|
@param phoneNumber The phone number to be verified.
|
|
@param callback The callback to be invoked on the global work queue when the flow is
|
|
finished.
|
|
*/
|
|
private func verifyClAndSendVerificationCode(toPhoneNumber phoneNumber: String,
|
|
retryOnInvalidAppCredential: Bool,
|
|
multiFactorSession session: MultiFactorSession?,
|
|
uiDelegate: AuthUIDelegate?,
|
|
callback: @escaping (String?, Error?) -> Void) {
|
|
if let settings = auth.settings,
|
|
settings.isAppVerificationDisabledForTesting {
|
|
let request = SendVerificationCodeRequest(
|
|
phoneNumber: phoneNumber,
|
|
appCredential: nil,
|
|
reCAPTCHAToken: nil,
|
|
requestConfiguration: auth.requestConfiguration
|
|
)
|
|
|
|
AuthBackend.post(withRequest: request) { response, error in
|
|
callback((response as? SendVerificationCodeResponse)?.verificationID, error)
|
|
}
|
|
return
|
|
}
|
|
guard let session else {
|
|
verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
uiDelegate: uiDelegate,
|
|
callback: callback)
|
|
return
|
|
}
|
|
|
|
verifyClient(withUIDelegate: uiDelegate) { appCredential, reCAPTCHAToken, error in
|
|
if let error {
|
|
callback(nil, error)
|
|
return
|
|
}
|
|
let startMFARequestInfo = AuthProtoStartMFAPhoneRequestInfo(phoneNumber: phoneNumber,
|
|
appCredential: appCredential,
|
|
reCAPTCHAToken: reCAPTCHAToken)
|
|
|
|
let request: AuthRPCRequest = (session.idToken != nil) ?
|
|
StartMFAEnrollmentRequest(idToken: session.idToken,
|
|
enrollmentInfo: startMFARequestInfo,
|
|
requestConfiguration: self.auth.requestConfiguration) :
|
|
StartMFASignInRequest(MFAPendingCredential: session.mfaPendingCredential,
|
|
MFAEnrollmentID: session.multiFactorInfo?.uid,
|
|
signInInfo: startMFARequestInfo,
|
|
requestConfiguration: self.auth.requestConfiguration)
|
|
|
|
AuthBackend.post(withRequest: request) { response, error in
|
|
if let error {
|
|
self.handleVerifyErrorWithRetry(error: error,
|
|
phoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: retryOnInvalidAppCredential,
|
|
multiFactorSession: session,
|
|
uiDelegate: uiDelegate,
|
|
callback: callback)
|
|
return
|
|
}
|
|
callback((response as? StartMFAEnrollmentResponse)?.enrollmentResponse?.sessionInfo, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func handleVerifyErrorWithRetry(error: Error,
|
|
phoneNumber: String,
|
|
retryOnInvalidAppCredential: Bool,
|
|
multiFactorSession session: MultiFactorSession?,
|
|
uiDelegate: AuthUIDelegate?,
|
|
callback: @escaping (String?, Error?) -> Void) {
|
|
if (error as NSError).code == AuthErrorCode.invalidAppCredential.rawValue {
|
|
if retryOnInvalidAppCredential {
|
|
auth.appCredentialManager.clearCredential()
|
|
verifyClAndSendVerificationCode(toPhoneNumber: phoneNumber,
|
|
retryOnInvalidAppCredential: false,
|
|
multiFactorSession: session,
|
|
uiDelegate: uiDelegate,
|
|
callback: callback)
|
|
return
|
|
}
|
|
callback(nil, AuthErrorUtils.unexpectedResponse(deserializedResponse: nil,
|
|
underlyingError: error))
|
|
return
|
|
}
|
|
callback(nil, error)
|
|
}
|
|
|
|
/** @fn
|
|
@brief Continues the flow to verify the client via silent push notification.
|
|
@param completion The callback to be invoked when the client verification flow is finished.
|
|
*/
|
|
private func verifyClient(withUIDelegate uiDelegate: AuthUIDelegate?,
|
|
completion: @escaping (AuthAppCredential?, String?, Error?) -> Void) {
|
|
// Remove the simulator check below after FCM supports APNs in simulators
|
|
#if targetEnvironment(simulator)
|
|
let environment = ProcessInfo().environment
|
|
if environment["XCTestConfigurationFilePath"] == nil {
|
|
reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate, completion: completion)
|
|
return
|
|
}
|
|
#endif
|
|
if let credential = auth.appCredentialManager.credential {
|
|
completion(credential, nil, nil)
|
|
return
|
|
}
|
|
auth.tokenManager.getToken { token, error in
|
|
guard let token else {
|
|
self.reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate, completion: completion)
|
|
return
|
|
}
|
|
let request = VerifyClientRequest(withAppToken: token.string,
|
|
isSandbox: token.type == AuthAPNSTokenType.sandbox,
|
|
requestConfiguration: self.auth.requestConfiguration)
|
|
AuthBackend.post(withRequest: request) { response, error in
|
|
if let error {
|
|
let nserror = error as NSError
|
|
// reCAPTCHA Flow if it's an invalid app credential or a missing app token.
|
|
if (nserror.code == AuthErrorCode.internalError.rawValue &&
|
|
(nserror.userInfo[NSUnderlyingErrorKey] as? NSError)?.code ==
|
|
AuthErrorCode.invalidAppCredential.rawValue) ||
|
|
nserror.code == AuthErrorCode.missingAppToken.rawValue {
|
|
self.reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate, completion: completion)
|
|
return
|
|
} else {
|
|
completion(nil, nil, error)
|
|
return
|
|
}
|
|
}
|
|
guard let verifyResponse = response as? VerifyClientResponse,
|
|
let receipt = verifyResponse.receipt,
|
|
let timeout = verifyResponse.suggestedTimeOutDate?.timeIntervalSinceNow else {
|
|
fatalError("Internal Auth Error: invalid VerifyClientResponse")
|
|
}
|
|
self.auth.appCredentialManager.didStartVerification(withReceipt: receipt,
|
|
timeout: timeout) { credential in
|
|
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.")
|
|
self.reCAPTCHAFlowWithUIDelegate(withUIDelegate: uiDelegate, completion: completion)
|
|
return
|
|
}
|
|
completion(credential, nil, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @fn
|
|
@brief Continues the flow to verify the client via silent push notification.
|
|
@param completion The callback to be invoked when the client verification flow is finished.
|
|
*/
|
|
private func reCAPTCHAFlowWithUIDelegate(withUIDelegate uiDelegate: AuthUIDelegate?,
|
|
completion: @escaping (AuthAppCredential?, String?,
|
|
Error?) -> Void) {
|
|
let eventID = AuthWebUtils.randomString(withLength: 10)
|
|
reCAPTCHAURL(withEventID: eventID) { reCAPTCHAURL, error in
|
|
if let error = error {
|
|
completion(nil, nil, error)
|
|
return
|
|
}
|
|
guard let reCAPTCHAURL = reCAPTCHAURL 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
|
|
)
|
|
}
|
|
self.auth.authURLPresenter.present(reCAPTCHAURL,
|
|
uiDelegate: uiDelegate,
|
|
callbackMatcher: callbackMatcher) { callbackURL, error in
|
|
if let error = error {
|
|
completion(nil, nil, error)
|
|
return
|
|
}
|
|
do {
|
|
let reCAPTHAtoken = try self.reCAPTCHAToken(forURL: callbackURL)
|
|
completion(nil, reCAPTHAtoken, nil)
|
|
} catch {
|
|
completion(nil, nil, error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
@brief Parses the reCAPTCHA URL and returns the reCAPTCHA token.
|
|
@param URL The url to be parsed for a reCAPTCHA token.
|
|
@param error The error that occurred if any.
|
|
@return 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)
|
|
}
|
|
|
|
/** @fn
|
|
@brief Constructs a URL used for opening a reCAPTCHA app verification flow using a given event
|
|
ID.
|
|
@param eventID The event ID used for this purpose.
|
|
@param completion The callback invoked after the URL has been constructed or an error
|
|
has been encountered.
|
|
*/
|
|
private func reCAPTCHAURL(withEventID eventID: String,
|
|
completion: @escaping ((URL?, Error?) -> Void)) {
|
|
AuthWebUtils
|
|
.fetchAuthDomain(withRequestConfiguration: auth.requestConfiguration) { authDomain, error in
|
|
if let error = error {
|
|
completion(nil, error)
|
|
return
|
|
}
|
|
if let authDomain = authDomain {
|
|
let bundleID = Bundle.main.bundleIdentifier
|
|
let clientID = self.auth.app?.options.clientID
|
|
let appID = self.auth.app?.options.googleAppID
|
|
let apiKey = self.auth.requestConfiguration.apiKey
|
|
let appCheck = self.auth.requestConfiguration.appCheck
|
|
var queryItems = [URLQueryItem(name: "apiKey", value: apiKey),
|
|
URLQueryItem(name: "authType", value: self.kAuthTypeVerifyApp),
|
|
URLQueryItem(name: "ibi", value: bundleID ?? ""),
|
|
URLQueryItem(name: "v", value: AuthBackend.authUserAgent()),
|
|
URLQueryItem(name: "eventId", value: eventID)]
|
|
if self.usingClientIDScheme {
|
|
queryItems.append(URLQueryItem(name: "clientId", value: clientID))
|
|
} else {
|
|
queryItems.append(URLQueryItem(name: "appId", value: appID))
|
|
}
|
|
if let languageCode = self.auth.requestConfiguration.languageCode {
|
|
queryItems.append(URLQueryItem(name: "hl", value: languageCode))
|
|
}
|
|
var components = URLComponents(string: "https://\(authDomain)/__/auth/handler?")
|
|
components?.queryItems = queryItems
|
|
|
|
if let appCheck {
|
|
appCheck.getToken(forcingRefresh: false) { tokenResult in
|
|
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
|
|
completion(components?.url, nil)
|
|
}
|
|
} else {
|
|
completion(components?.url, nil)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private let auth: Auth
|
|
private let callbackScheme: String
|
|
private let usingClientIDScheme: Bool
|
|
|
|
private 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
|
|
}
|