| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226 |
- // Copyright 2024 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
- #if COCOAPODS
- @_implementationOnly import GoogleUtilities
- #else
- @_implementationOnly import GoogleUtilities_AppDelegateSwizzler
- @_implementationOnly import GoogleUtilities_Environment
- #endif
- #if canImport(UIKit)
- import UIKit
- #endif
- @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
- actor AuthWorker {
- let requestConfiguration: AuthRequestConfiguration
- func getLanguageCode() -> String? {
- return requestConfiguration.languageCode
- }
- func setLanguageCode(_ code: String?) {
- requestConfiguration.languageCode = code
- }
- /// The manager for APNs tokens used by phone number auth.
- var tokenManager: AuthAPNSTokenManager!
- func tokenManagerCancel(error: Error) {
- tokenManager.cancel(withError: error)
- }
- func tokenManagerSet(_ token: Data, type: AuthAPNSTokenType) {
- tokenManager.token = AuthAPNSToken(withData: token, type: type)
- }
- func tokenManagerGet() -> AuthAPNSTokenManager {
- return tokenManager
- }
- func getToken(forcingRefresh forceRefresh: Bool) async throws -> String? {
- // Enable token auto-refresh if not already enabled.
- guard let auth = requestConfiguration.auth else {
- return nil
- }
- auth.getTokenInternal(forcingRefresh: forceRefresh)
- // Call back with 'nil' if there is no current user.
- guard let currentUser = auth.currentUser else {
- return nil
- }
- return try await currentUser.internalGetToken(forceRefresh: forceRefresh)
- }
- /// Only for testing
- func tokenManagerInit(_ manager: AuthAPNSTokenManager) {
- tokenManager = manager
- }
- func fetchSignInMethods(forEmail email: String) async throws -> [String] {
- let request = CreateAuthURIRequest(identifier: email,
- continueURI: "http:www.google.com",
- requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- return response.signinMethods ?? []
- }
- func signIn(withEmail email: String, password: String) async throws -> AuthDataResult {
- let credential = EmailAuthCredential(withEmail: email, password: password)
- return try await internalSignInAndRetrieveData(withCredential: credential,
- isReauthentication: false)
- }
- func signIn(withEmail email: String, link: String) async throws -> AuthDataResult {
- let credential = EmailAuthCredential(withEmail: email, link: link)
- return try await internalSignInAndRetrieveData(withCredential: credential,
- isReauthentication: false)
- }
- func signIn(with credential: AuthCredential) async throws -> AuthDataResult {
- return try await internalSignInAndRetrieveData(withCredential: credential,
- isReauthentication: false)
- }
- #if os(iOS)
- func signIn(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
- let credential = try await provider.credential(with: uiDelegate)
- return try await internalSignInAndRetrieveData(
- withCredential: credential,
- isReauthentication: false
- )
- }
- #endif
- func signInAnonymously() async throws -> AuthDataResult {
- if let currentUser = requestConfiguration.auth?.currentUser,
- currentUser.isAnonymous {
- return AuthDataResult(withUser: currentUser, additionalUserInfo: nil)
- }
- let request = SignUpNewUserRequest(requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- let user = try await completeSignIn(
- withAccessToken: response.idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: true
- )
- // TODO: The ObjC implementation passed a nil providerID to the nonnull providerID
- let additionalUserInfo = AdditionalUserInfo(providerID: "",
- profile: nil,
- username: nil,
- isNewUser: true)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- func signIn(withCustomToken token: String) async throws -> AuthDataResult {
- let request = VerifyCustomTokenRequest(token: token,
- requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- let user = try await completeSignIn(
- withAccessToken: response.idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false
- )
- // TODO: The ObjC implementation passed a nil providerID to the nonnull providerID
- let additionalUserInfo = AdditionalUserInfo(providerID: "",
- profile: nil,
- username: nil,
- isNewUser: response.isNewUser)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- func createUser(withEmail email: String, password: String) async throws -> AuthDataResult {
- let request = SignUpNewUserRequest(email: email,
- password: password,
- displayName: nil,
- idToken: nil,
- requestConfiguration: requestConfiguration)
- #if os(iOS)
- let response = try await injectRecaptcha(request: request,
- action: AuthRecaptchaAction.signUpPassword)
- #else
- let response = try await AuthBackend.call(with: request)
- #endif
- let user = try await completeSignIn(
- withAccessToken: response.idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false
- )
- let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
- profile: nil,
- username: nil,
- isNewUser: true)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- func confirmPasswordReset(withCode code: String, newPassword: String) async throws {
- let request = ResetPasswordRequest(oobCode: code,
- newPassword: newPassword,
- requestConfiguration: requestConfiguration)
- _ = try await AuthBackend.call(with: request)
- }
- func checkActionCode(_ code: String) async throws -> ActionCodeInfo {
- let request = ResetPasswordRequest(oobCode: code,
- newPassword: nil,
- requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- let operation = ActionCodeInfo.actionCodeOperation(forRequestType: response.requestType)
- guard let email = response.email else {
- fatalError("Internal Auth Error: Failed to get a ResetPasswordResponse")
- }
- return ActionCodeInfo(withOperation: operation,
- email: email,
- newEmail: response.verifiedEmail)
- }
- func verifyPasswordResetCode(_ code: String) async throws -> String {
- let info = try await checkActionCode(code)
- return info.email
- }
- func applyActionCode(_ code: String) async throws {
- let request = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
- request.oobCode = code
- _ = try await AuthBackend.call(with: request)
- }
- func sendPasswordReset(withEmail email: String,
- actionCodeSettings: ActionCodeSettings? = nil) async throws {
- let request = GetOOBConfirmationCodeRequest.passwordResetRequest(
- email: email,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- #if os(iOS)
- _ = try await injectRecaptcha(request: request,
- action: AuthRecaptchaAction.getOobCode)
- #else
- _ = try await AuthBackend.call(with: request)
- #endif
- }
- func sendSignInLink(toEmail email: String,
- actionCodeSettings: ActionCodeSettings) async throws {
- let request = GetOOBConfirmationCodeRequest.signInWithEmailLinkRequest(
- email,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- #if os(iOS)
- _ = try await injectRecaptcha(request: request,
- action: AuthRecaptchaAction.getOobCode)
- #else
- _ = try await AuthBackend.call(with: request)
- #endif
- }
- func signOut() throws {
- guard requestConfiguration.auth?.currentUser != nil else {
- return
- }
- try updateCurrentUser(nil, byForce: false, savingToDisk: true)
- }
- func updateCurrentUser(_ user: User) async throws {
- if user.requestConfiguration.apiKey != requestConfiguration.apiKey {
- // If the API keys are different, then we need to confirm that the user belongs to the same
- // project before proceeding.
- user.requestConfiguration = requestConfiguration
- try await user.reload()
- }
- try updateCurrentUser(user, byForce: true, savingToDisk: true)
- }
- /// Continue with the rest of the Auth object initialization in the worker actor.
- func protectedDataInitialization(_ keychainStorageProvider: AuthKeychainStorage) {
- // Load current user from Keychain.
- guard let auth = requestConfiguration.auth else {
- return
- }
- if let keychainServiceName = Auth.keychainServiceName(forAppName: auth.firebaseAppName) {
- auth.keychainServices = AuthKeychainServices(service: keychainServiceName,
- storage: keychainStorageProvider)
- auth.storedUserManager = AuthStoredUserManager(
- serviceName: keychainServiceName,
- keychainServices: auth.keychainServices
- )
- }
- do {
- if let storedUserAccessGroup = auth.storedUserManager.getStoredUserAccessGroup() {
- try auth.internalUseUserAccessGroup(storedUserAccessGroup)
- } else {
- let user = try auth.getUser()
- try updateCurrentUser(user, byForce: false, savingToDisk: false)
- if let user {
- auth.tenantID = user.tenantID
- auth.lastNotifiedUserToken = user.rawAccessToken()
- }
- }
- } catch {
- #if canImport(UIKit)
- if (error as NSError).code == AuthErrorCode.keychainError.rawValue {
- // If there's a keychain error, assume it is due to the keychain being accessed
- // before the device is unlocked as a result of prewarming, and listen for the
- // UIApplicationProtectedDataDidBecomeAvailable notification.
- auth.addProtectedDataDidBecomeAvailableObserver()
- }
- #endif
- AuthLog.logError(code: "I-AUT000001",
- message: "Error loading saved user when starting up: \(error)")
- }
- #if os(iOS)
- if GULAppEnvironmentUtil.isAppExtension() {
- // iOS App extensions should not call [UIApplication sharedApplication], even if
- // UIApplication responds to it.
- return
- }
- // Using reflection here to avoid build errors in extensions.
- let sel = NSSelectorFromString("sharedApplication")
- guard UIApplication.responds(to: sel),
- let rawApplication = UIApplication.perform(sel),
- let application = rawApplication.takeUnretainedValue() as? UIApplication else {
- return
- }
- // Initialize for phone number auth.
- tokenManager = AuthAPNSTokenManager(withApplication: application)
- auth.appCredentialManager = AuthAppCredentialManager(withKeychain: auth.keychainServices)
- auth.notificationManager = AuthNotificationManager(
- withApplication: application,
- appCredentialManager: auth.appCredentialManager
- )
- GULAppDelegateSwizzler.registerAppDelegateInterceptor(auth)
- GULSceneDelegateSwizzler.registerSceneDelegateInterceptor(auth)
- #endif
- }
- // MARK: User.swift implementations
- func updateEmail(user: User,
- email: String?,
- password: String?) async throws {
- let hadEmailPasswordCredential = user.hasEmailPasswordCredential
- try await executeUserUpdateWithChanges(user: user) { userAccount, request in
- if let email {
- request.email = email
- }
- if let password {
- request.password = password
- }
- }
- if let email {
- user.email = email
- }
- if user.email != nil {
- guard !hadEmailPasswordCredential else {
- if let error = user.updateKeychain() {
- throw error
- }
- return
- }
- // The list of providers need to be updated for the newly added email-password provider.
- let accessToken = try await user.internalGetToken()
- let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken,
- requestConfiguration: requestConfiguration)
- do {
- let accountInfoResponse = try await AuthBackend.call(with: getAccountInfoRequest)
- if let users = accountInfoResponse.users {
- for userAccountInfo in users {
- // Set the account to non-anonymous if there are any providers, even if
- // they're not email/password ones.
- if let providerUsers = userAccountInfo.providerUserInfo {
- if providerUsers.count > 0 {
- user.isAnonymous = false
- for providerUserInfo in providerUsers {
- if providerUserInfo.providerID == EmailAuthProvider.id {
- user.hasEmailPasswordCredential = true
- break
- }
- }
- }
- }
- }
- }
- user.update(withGetAccountInfoResponse: accountInfoResponse)
- if let error = user.updateKeychain() {
- throw error
- }
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- }
- #if os(iOS)
- /// Updates the phone number for the user. On success, the cached user profile data is updated.
- ///
- /// Invoked asynchronously on the global work queue in the future.
- /// - Parameter credential: The new phone number credential corresponding to the phone
- /// number to be added to the Firebase account. If a phone number is already linked to the
- /// account, this new phone number will replace it.
- /// - Parameter isLinkOperation: Boolean value indicating whether or not this is a link
- /// operation.
- func updateOrLinkPhoneNumber(user: User, credential: PhoneAuthCredential,
- isLinkOperation: Bool) async throws {
- let accessToken = try await user.internalGetToken()
- guard let configuration = user.auth?.requestConfiguration else {
- fatalError("Auth Internal Error: nil value for VerifyPhoneNumberRequest initializer")
- }
- switch credential.credentialKind {
- case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
- case let .verification(verificationID, code):
- let operation = isLinkOperation ? AuthOperationType.link : AuthOperationType.update
- let request = VerifyPhoneNumberRequest(verificationID: verificationID,
- verificationCode: code,
- operation: operation,
- requestConfiguration: configuration)
- request.accessToken = accessToken
- do {
- let verifyResponse = try await AuthBackend.call(with: request)
- guard let idToken = verifyResponse.idToken,
- let refreshToken = verifyResponse.refreshToken else {
- fatalError("Internal Auth Error: missing token in internalUpdateOrLinkPhoneNumber")
- }
- user.tokenService = SecureTokenService(
- withRequestConfiguration: configuration,
- accessToken: idToken,
- accessTokenExpirationDate: verifyResponse.approximateExpirationDate,
- refreshToken: refreshToken
- )
- // Get account info to update cached user info.
- _ = try await getAccountInfoRefreshingCache(user)
- user.isAnonymous = false
- if let error = user.updateKeychain() {
- throw error
- }
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- }
- #endif
- /// Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
- /// atomically in regards to other calls to this method.
- /// - Parameter changeBlock: A block responsible for mutating a template `SetAccountInfoRequest`
- /// - Parameter callback: A block to invoke when the change is complete. Invoked asynchronously on
- /// the auth global work queue in the future.
- private func executeUserUpdateWithChanges(user: User,
- changeBlock: @escaping (GetAccountInfoResponseUser,
- SetAccountInfoRequest)
- -> Void) async throws {
- let userAccountInfo = try await getAccountInfoRefreshingCache(user)
- let accessToken = try await user.internalGetToken()
- // Mutate setAccountInfoRequest in block
- let setAccountInfoRequest = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
- setAccountInfoRequest.accessToken = accessToken
- changeBlock(userAccountInfo, setAccountInfoRequest)
- do {
- let accountInfoResponse = try await AuthBackend.call(with: setAccountInfoRequest)
- if let idToken = accountInfoResponse.idToken,
- let refreshToken = accountInfoResponse.refreshToken {
- let tokenService = SecureTokenService(
- withRequestConfiguration: requestConfiguration,
- accessToken: idToken,
- accessTokenExpirationDate: accountInfoResponse.approximateExpirationDate,
- refreshToken: refreshToken
- )
- try await user.setTokenService(tokenService: tokenService)
- }
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- /// Gets the users' account data from the server, updating our local values.
- /// - Parameter callback: Invoked when the request to getAccountInfo has completed, or when an
- /// error has been detected. Invoked asynchronously on the auth global work queue in the future.
- func getAccountInfoRefreshingCache(_ user: User) async throws
- -> GetAccountInfoResponseUser {
- let token = try await user.internalGetToken()
- let request = GetAccountInfoRequest(accessToken: token,
- requestConfiguration: requestConfiguration)
- do {
- let accountInfoResponse = try await AuthBackend.call(with: request)
- user.update(withGetAccountInfoResponse: accountInfoResponse)
- if let error = user.updateKeychain() {
- throw error
- }
- return (accountInfoResponse.users?.first)!
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- func reauthenticate(with credential: AuthCredential) async throws -> AuthDataResult {
- do {
- let authResult = try await internalSignInAndRetrieveData(
- withCredential: credential,
- isReauthentication: true
- )
- let user = authResult.user
- guard user.uid == requestConfiguration.auth?.getUserID() else {
- throw AuthErrorUtils.userMismatchError()
- }
- try await user.setTokenService(tokenService: user.tokenService)
- return authResult
- } catch {
- if (error as NSError).code == AuthErrorCode.userNotFound.rawValue {
- throw AuthErrorUtils.userMismatchError()
- }
- throw error
- }
- }
- #if os(iOS)
- func reauthenticate(with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
- let credential = try await provider.credential(with: uiDelegate)
- return try await reauthenticate(with: credential)
- }
- #endif
- func getIDTokenResult(user: User,
- forcingRefresh forceRefresh: Bool) async throws -> AuthTokenResult {
- let token = try await user.internalGetToken(forceRefresh: forceRefresh)
- let tokenResult = try AuthTokenResult.tokenResult(token: token)
- AuthLog.logDebug(code: "I-AUT000017", message: "Actual token expiration date: " +
- "\(String(describing: tokenResult.expirationDate))," +
- "current date: \(Date())")
- return tokenResult
- }
- func link(user: User, with credential: AuthCredential) async throws -> AuthDataResult {
- if user.providerDataRaw[credential.provider] != nil {
- throw AuthErrorUtils.providerAlreadyLinkedError()
- }
- if let emailCredential = credential as? EmailAuthCredential {
- return try await link(user: user, withEmailCredential: emailCredential)
- }
- #if !os(watchOS)
- if let gameCenterCredential = credential as? GameCenterAuthCredential {
- return try await link(user: user, withGameCenterCredential: gameCenterCredential)
- }
- #endif
- #if os(iOS)
- if let phoneCredential = credential as? PhoneAuthCredential {
- return try await link(user: user, withPhoneCredential: phoneCredential)
- }
- #endif
- let accessToken = try await user.internalGetToken()
- let request = VerifyAssertionRequest(providerID: credential.provider,
- requestConfiguration: requestConfiguration)
- credential.prepare(request)
- request.accessToken = accessToken
- do {
- let response = try await AuthBackend.call(with: request)
- guard let idToken = response.idToken,
- let refreshToken = response.refreshToken,
- let providerID = response.providerID else {
- fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
- }
- try await updateTokenAndRefreshUser(user: user,
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate)
- let updatedOAuthCredential = OAuthCredential(withVerifyAssertionResponse: response)
- let additionalUserInfo = AdditionalUserInfo(providerID: providerID,
- profile: response.profile,
- username: response.username,
- isNewUser: response.isNewUser)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo,
- credential: updatedOAuthCredential)
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- func link(user: User, with provider: FederatedAuthProvider,
- uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
- let credential = try await provider.credential(with: uiDelegate)
- return try await link(user: user, with: credential)
- }
- func unlink(user: User, fromProvider provider: String) async throws -> User {
- let accessToken = try await user.internalGetToken()
- let request = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
- request.accessToken = accessToken
- if user.providerDataRaw[provider] == nil {
- throw AuthErrorUtils.noSuchProviderError()
- }
- request.deleteProviders = [provider]
- do {
- let response = try await AuthBackend.call(with: request)
- // We can't just use the provider info objects in SetAccountInfoResponse
- // because they don't have localID and email fields. Remove the specific
- // provider manually.
- user.providerDataRaw.removeValue(forKey: provider)
- if provider == EmailAuthProvider.id {
- user.hasEmailPasswordCredential = false
- }
- #if os(iOS)
- // After successfully unlinking a phone auth provider, remove the phone number
- // from the cached user info.
- if provider == PhoneAuthProvider.id {
- user.phoneNumber = nil
- }
- #endif
- if let idToken = response.idToken,
- let refreshToken = response.refreshToken {
- let tokenService = SecureTokenService(
- withRequestConfiguration: requestConfiguration,
- accessToken: idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: refreshToken
- )
- try await user.setTokenService(tokenService: tokenService)
- return user
- }
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- if let error = user.updateKeychain() {
- throw error
- }
- return user
- }
- func sendEmailVerification(user: User,
- with actionCodeSettings: ActionCodeSettings?) async throws {
- let accessToken = try await user.internalGetToken()
- let request = GetOOBConfirmationCodeRequest.verifyEmailRequest(
- accessToken: accessToken,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- do {
- _ = try await AuthBackend.call(with: request)
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- func sendEmailVerification(user: User,
- beforeUpdatingEmail newEmail: String,
- actionCodeSettings: ActionCodeSettings?) async throws {
- let accessToken = try await user.internalGetToken()
- let request = GetOOBConfirmationCodeRequest.verifyBeforeUpdateEmail(
- accessToken: accessToken,
- newEmail: newEmail,
- actionCodeSettings: actionCodeSettings,
- requestConfiguration: requestConfiguration
- )
- do {
- _ = try await AuthBackend.call(with: request)
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- func delete(user: User) async throws {
- let accessToken = try await user.internalGetToken()
- let request = DeleteAccountRequest(localID: user.uid, accessToken: accessToken,
- requestConfiguration: requestConfiguration)
- _ = try await AuthBackend.call(with: request)
- try user.auth?.signOutByForce(withUserID: user.uid)
- }
- func commitChanges(changeRequest: UserProfileChangeRequest) async throws {
- if changeRequest.consumed {
- fatalError("Internal Auth Error: commitChanges should only be called once.")
- }
- changeRequest.consumed = true
- // Return fast if there is nothing to update:
- if !changeRequest.photoURLWasSet, !changeRequest.displayNameWasSet {
- return
- }
- let displayName = changeRequest.displayName
- let displayNameWasSet = changeRequest.displayNameWasSet
- let photoURL = changeRequest.photoURL
- let photoURLWasSet = changeRequest.photoURLWasSet
- try await executeUserUpdateWithChanges(user: changeRequest.user) { _, request in
- if photoURLWasSet {
- request.photoURL = photoURL
- }
- if displayNameWasSet {
- request.displayName = displayName
- }
- }
- if displayNameWasSet {
- changeRequest.user.displayName = displayName
- }
- if photoURLWasSet {
- changeRequest.user.photoURL = photoURL
- }
- if let error = changeRequest.user.updateKeychain() {
- throw error
- }
- }
- private func link(user: User,
- withEmailCredential emailCredential: EmailAuthCredential) async throws
- -> AuthDataResult {
- if user.hasEmailPasswordCredential {
- throw AuthErrorUtils.providerAlreadyLinkedError()
- }
- switch emailCredential.emailType {
- case let .password(password):
- let result = AuthDataResult(withUser: user, additionalUserInfo: nil)
- return try await link(
- user: user,
- withEmail: emailCredential.email,
- password: password,
- authResult: result
- )
- case let .link(link):
- let accessToken = try? await user.internalGetToken()
- var queryItems = AuthWebUtils.parseURL(link)
- if link.count == 0 {
- if let urlComponents = URLComponents(string: link),
- let query = urlComponents.query {
- queryItems = AuthWebUtils.parseURL(query)
- }
- }
- guard let actionCode = queryItems["oobCode"] else {
- fatalError("Internal Auth Error: Missing oobCode")
- }
- let request = EmailLinkSignInRequest(email: emailCredential.email,
- oobCode: actionCode,
- requestConfiguration: requestConfiguration)
- request.idToken = accessToken
- let response = try await AuthBackend.call(with: request)
- guard let idToken = response.idToken,
- let refreshToken = response.refreshToken else {
- fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
- }
- try await updateTokenAndRefreshUser(
- user: user,
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate
- )
- return AuthDataResult(withUser: user, additionalUserInfo: nil)
- }
- }
- private func link(user: User,
- withEmail email: String,
- password: String,
- authResult: AuthDataResult) async throws -> AuthDataResult {
- let accessToken = try await user.internalGetToken()
- do {
- let request = SignUpNewUserRequest(email: email,
- password: password,
- displayName: nil,
- idToken: accessToken,
- requestConfiguration: requestConfiguration)
- #if os(iOS)
- let response = try await injectRecaptcha(request: request,
- action: AuthRecaptchaAction.signUpPassword)
- #else
- let response = try await AuthBackend.call(with: request)
- #endif
- guard let refreshToken = response.refreshToken,
- let idToken = response.idToken else {
- fatalError("Internal auth error: Invalid SignUpNewUserResponse")
- }
- // Update the new token and refresh user info again.
- user.tokenService = SecureTokenService(
- withRequestConfiguration: requestConfiguration,
- accessToken: idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: refreshToken
- )
- let accessToken = try await user.internalGetToken()
- let getAccountInfoRequest = GetAccountInfoRequest(
- accessToken: accessToken,
- requestConfiguration: requestConfiguration
- )
- let accountResponse = try await AuthBackend.call(with: getAccountInfoRequest)
- user.isAnonymous = false
- user.update(withGetAccountInfoResponse: accountResponse)
- if let keychainError = user.updateKeychain() {
- throw keychainError
- }
- return authResult
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- }
- #if !os(watchOS)
- private func link(user: User,
- withGameCenterCredential gameCenterCredential: GameCenterAuthCredential) async throws
- -> AuthDataResult {
- let accessToken = try await user.internalGetToken()
- guard let publicKeyURL = gameCenterCredential.publicKeyURL,
- let signature = gameCenterCredential.signature,
- let salt = gameCenterCredential.salt else {
- fatalError("Internal Auth Error: Nil value field for SignInWithGameCenterRequest")
- }
- let request = SignInWithGameCenterRequest(playerID: gameCenterCredential.playerID,
- teamPlayerID: gameCenterCredential.teamPlayerID,
- gamePlayerID: gameCenterCredential.gamePlayerID,
- publicKeyURL: publicKeyURL,
- signature: signature,
- salt: salt,
- timestamp: gameCenterCredential.timestamp,
- displayName: gameCenterCredential.displayName,
- requestConfiguration: requestConfiguration)
- request.accessToken = accessToken
- let response = try await AuthBackend.call(with: request)
- guard let idToken = response.idToken,
- let refreshToken = response.refreshToken else {
- fatalError("Internal Auth Error: missing token in link(withGameCredential")
- }
- try await updateTokenAndRefreshUser(user: user,
- idToken: idToken,
- refreshToken: refreshToken,
- expirationDate: response.approximateExpirationDate)
- return AuthDataResult(withUser: user, additionalUserInfo: nil)
- }
- #endif
- #if os(iOS)
- private func link(user: User,
- withPhoneCredential phoneCredential: PhoneAuthCredential) async throws
- -> AuthDataResult {
- try await updateOrLinkPhoneNumber(user: user,
- credential: phoneCredential,
- isLinkOperation: true)
- return AuthDataResult(withUser: user, additionalUserInfo: nil)
- }
- #endif
- // Update the new token and refresh user info again.
- private func updateTokenAndRefreshUser(user: User,
- idToken: String,
- refreshToken: String,
- expirationDate: Date?) async throws {
- user.tokenService = SecureTokenService(
- withRequestConfiguration: requestConfiguration,
- accessToken: idToken,
- accessTokenExpirationDate: expirationDate,
- refreshToken: refreshToken
- )
- let accessToken = try await user.internalGetToken()
- let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken,
- requestConfiguration: requestConfiguration)
- do {
- let response = try await AuthBackend.call(with: getAccountInfoRequest)
- user.isAnonymous = false
- user.update(withGetAccountInfoResponse: response)
- } catch {
- user.signOutIfTokenIsInvalid(withError: error)
- throw error
- }
- if let error = user.updateKeychain() {
- throw error
- }
- }
- /// Update the current user; initializing the user's internal properties correctly, and
- /// optionally saving the user to disk.
- ///
- /// This method is called during: sign in and sign out events, as well as during class
- /// initialization time. The only time the saveToDisk parameter should be set to NO is during
- /// class initialization time because the user was just read from disk.
- /// - Parameter user: The user to use as the current user (including nil, which is passed at sign
- /// out time.)
- /// - Parameter saveToDisk: Indicates the method should persist the user data to disk.
- func updateCurrentUser(_ user: User?, byForce force: Bool,
- savingToDisk saveToDisk: Bool) throws {
- if user == requestConfiguration.auth?.currentUser {
- // TODO: local
- requestConfiguration.auth?.possiblyPostAuthStateChangeNotification()
- }
- if let user {
- if user.tenantID != requestConfiguration.auth?.tenantID {
- let error = AuthErrorUtils.tenantIDMismatchError()
- throw error
- }
- }
- var throwError: Error?
- if saveToDisk {
- do {
- // TODO: call local saveSuer
- try requestConfiguration.auth?.saveUser(user)
- } catch {
- throwError = error
- }
- }
- if throwError == nil || force {
- requestConfiguration.auth?.currentUser = user
- // TODO:
- requestConfiguration.auth?.possiblyPostAuthStateChangeNotification()
- }
- if let throwError {
- throw throwError
- }
- }
- func useEmulator(withHost host: String, port: Int) async {
- // If host is an IPv6 address, it should be formatted with surrounding brackets.
- let formattedHost = host.contains(":") ? "[\(host)]" : host
- requestConfiguration.emulatorHostAndPort = "\(formattedHost):\(port)"
- #if os(iOS)
- requestConfiguration.auth?.settings?.appVerificationDisabledForTesting = true
- #endif
- }
- #if os(iOS)
- func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool {
- guard let auth = requestConfiguration.auth else {
- return false
- }
- return auth.notificationManager.canHandle(notification: userInfo)
- }
- func canHandle(_ url: URL) -> Bool {
- guard let auth = requestConfiguration.auth,
- let authURLPresenter = auth.authURLPresenter as? AuthURLPresenter else {
- return false
- }
- return authURLPresenter.canHandle(url: url)
- }
- #endif
- func autoTokenRefresh(accessToken: String, retry: Bool, delay: TimeInterval) async {
- try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
- guard let auth = requestConfiguration.auth,
- let currentUser = auth.currentUser else {
- return
- }
- let accessToken = currentUser.rawAccessToken()
- guard currentUser.rawAccessToken() == accessToken else {
- // Another auto refresh must have been scheduled, so keep _autoRefreshScheduled unchanged.
- return
- }
- auth.autoRefreshScheduled = false
- if auth.isAppInBackground {
- return
- }
- let uid = currentUser.uid
- do {
- _ = try await currentUser.internalGetToken(forceRefresh: true)
- if auth.currentUser?.uid != uid {
- return
- }
- } catch {
- // Kicks off exponential back off logic to retry failed attempt. Starts with one minute
- // delay (60 seconds) if this is the first failed attempt.
- let rescheduleDelay = retry ? min(delay * 2, 16 * 60) : 60
- auth.scheduleAutoTokenRefresh(withDelay: rescheduleDelay, retry: true)
- }
- }
- func fetchAccessToken(user: User,
- forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) {
- if !forceRefresh, user.tokenService.hasValidAccessToken() {
- return (user.tokenService.accessToken, false)
- } else {
- AuthLog.logDebug(code: "I-AUT000017", message: "Fetching new token from backend.")
- return try await user.tokenService.requestAccessToken(retryIfExpired: true)
- }
- }
- private func internalSignInAndRetrieveData(withCredential credential: AuthCredential,
- isReauthentication: Bool) async throws
- -> AuthDataResult {
- if let emailCredential = credential as? EmailAuthCredential {
- // Special case for email/password credentials
- switch emailCredential.emailType {
- case let .link(link):
- // Email link sign in
- return try await internalSignInAndRetrieveData(withEmail: emailCredential.email, link: link)
- case let .password(password):
- // Email password sign in
- let user = try await internalSignInUser(
- withEmail: emailCredential.email,
- password: password
- )
- let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
- profile: nil,
- username: nil,
- isNewUser: false)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- }
- #if !os(watchOS)
- if let gameCenterCredential = credential as? GameCenterAuthCredential {
- return try await signInAndRetrieveData(withGameCenterCredential: gameCenterCredential)
- }
- #endif
- #if os(iOS)
- if let phoneCredential = credential as? PhoneAuthCredential {
- // Special case for phone auth credentials
- let operation = isReauthentication ? AuthOperationType.reauth :
- AuthOperationType.signUpOrSignIn
- let response = try await signIn(withPhoneCredential: phoneCredential,
- operation: operation)
- let user = try await completeSignIn(withAccessToken: response.idToken,
- accessTokenExpirationDate: response
- .approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false)
- let additionalUserInfo = AdditionalUserInfo(providerID: PhoneAuthProvider.id,
- profile: nil,
- username: nil,
- isNewUser: response.isNewUser)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- #endif
- let request = VerifyAssertionRequest(providerID: credential.provider,
- requestConfiguration: requestConfiguration)
- request.autoCreate = !isReauthentication
- credential.prepare(request)
- let response = try await AuthBackend.call(with: request)
- if response.needConfirmation {
- let email = response.email
- let credential = OAuthCredential(withVerifyAssertionResponse: response)
- throw AuthErrorUtils.accountExistsWithDifferentCredentialError(
- email: email,
- updatedCredential: credential
- )
- }
- guard let providerID = response.providerID, providerID.count > 0 else {
- throw AuthErrorUtils.unexpectedResponse(deserializedResponse: response)
- }
- let user = try await completeSignIn(withAccessToken: response.idToken,
- accessTokenExpirationDate: response
- .approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false)
- let additionalUserInfo = AdditionalUserInfo(providerID: providerID,
- profile: response.profile,
- username: response.username,
- isNewUser: response.isNewUser)
- let updatedOAuthCredential = OAuthCredential(withVerifyAssertionResponse: response)
- return AuthDataResult(withUser: user,
- additionalUserInfo: additionalUserInfo,
- credential: updatedOAuthCredential)
- }
- #if os(iOS)
- /// Signs in using a phone credential.
- /// - Parameter credential: The Phone Auth credential used to sign in.
- /// - Parameter operation: The type of operation for which this sign-in attempt is initiated.
- private func signIn(withPhoneCredential credential: PhoneAuthCredential,
- operation: AuthOperationType) async throws -> VerifyPhoneNumberResponse {
- switch credential.credentialKind {
- case let .phoneNumber(phoneNumber, temporaryProof):
- let request = VerifyPhoneNumberRequest(temporaryProof: temporaryProof,
- phoneNumber: phoneNumber,
- operation: operation,
- requestConfiguration: requestConfiguration)
- return try await AuthBackend.call(with: request)
- case let .verification(verificationID, code):
- guard verificationID.count > 0 else {
- throw AuthErrorUtils.missingVerificationIDError(message: nil)
- }
- guard code.count > 0 else {
- throw AuthErrorUtils.missingVerificationCodeError(message: nil)
- }
- let request = VerifyPhoneNumberRequest(verificationID: verificationID,
- verificationCode: code,
- operation: operation,
- requestConfiguration: requestConfiguration)
- return try await AuthBackend.call(with: request)
- }
- }
- #endif
- #if !os(watchOS)
- /// Signs in using a game center credential.
- /// - Parameter credential: The Game Center Auth Credential used to sign in.
- private func signInAndRetrieveData(withGameCenterCredential credential: GameCenterAuthCredential) async throws
- -> AuthDataResult {
- guard let publicKeyURL = credential.publicKeyURL,
- let signature = credential.signature,
- let salt = credential.salt else {
- fatalError(
- "Internal Auth Error: Game Center credential missing publicKeyURL, signature, or salt"
- )
- }
- let request = SignInWithGameCenterRequest(playerID: credential.playerID,
- teamPlayerID: credential.teamPlayerID,
- gamePlayerID: credential.gamePlayerID,
- publicKeyURL: publicKeyURL,
- signature: signature,
- salt: salt,
- timestamp: credential.timestamp,
- displayName: credential.displayName,
- requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- let user = try await completeSignIn(withAccessToken: response.idToken,
- accessTokenExpirationDate: response
- .approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false)
- let additionalUserInfo = AdditionalUserInfo(providerID: GameCenterAuthProvider.id,
- profile: nil,
- username: nil,
- isNewUser: response.isNewUser)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- #endif
- /// Signs in using an email and email sign-in link.
- /// - Parameter email: The user's email address.
- /// - Parameter link: The email sign-in link.
- private func internalSignInAndRetrieveData(withEmail email: String,
- link: String) async throws -> AuthDataResult {
- guard let auth = requestConfiguration.auth, auth.isSignIn(withEmailLink: link) else {
- fatalError("The link provided is not valid for email/link sign-in. Please check the link by " +
- "calling isSignIn(withEmailLink:) on the Auth instance before attempting to use it " +
- "for email/link sign-in.")
- }
- let queryItems = getQueryItems(link)
- guard let actionCode = queryItems["oobCode"] else {
- fatalError("Missing oobCode in link URL")
- }
- let request = EmailLinkSignInRequest(email: email,
- oobCode: actionCode,
- requestConfiguration: requestConfiguration)
- let response = try await AuthBackend.call(with: request)
- let user = try await completeSignIn(withAccessToken: response.idToken,
- accessTokenExpirationDate: response
- .approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false)
- let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
- profile: nil,
- username: nil,
- isNewUser: response.isNewUser)
- return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
- }
- private func getQueryItems(_ link: String) -> [String: String] {
- var queryItems = AuthWebUtils.parseURL(link)
- if queryItems.count == 0 {
- let urlComponents = URLComponents(string: link)
- if let query = urlComponents?.query {
- queryItems = AuthWebUtils.parseURL(query)
- }
- }
- return queryItems
- }
- private func internalSignInUser(withEmail email: String, password: String) async throws -> User {
- let request = VerifyPasswordRequest(email: email,
- password: password,
- requestConfiguration: requestConfiguration)
- if request.password.count == 0 {
- throw AuthErrorUtils.wrongPasswordError(message: nil)
- }
- #if os(iOS)
- let response = try await injectRecaptcha(request: request,
- action: AuthRecaptchaAction.signInWithPassword)
- #else
- let response = try await AuthBackend.call(with: request)
- #endif
- return try await completeSignIn(
- withAccessToken: response.idToken,
- accessTokenExpirationDate: response.approximateExpirationDate,
- refreshToken: response.refreshToken,
- anonymous: false
- )
- }
- #if os(iOS)
- func injectRecaptcha<T: AuthRPCRequest>(request: T,
- action: AuthRecaptchaAction) async throws -> T
- .Response {
- let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: requestConfiguration.auth)
- if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) {
- try await recaptchaVerifier.injectRecaptchaFields(request: request,
- provider: AuthRecaptchaProvider.password,
- action: action)
- } else {
- do {
- return try await AuthBackend.call(with: request)
- } catch {
- let nsError = error as NSError
- if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError,
- nsError.code == AuthErrorCode.internalError.rawValue,
- let messages = underlyingError
- .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable],
- let message = messages["message"] as? String,
- message.hasPrefix("MISSING_RECAPTCHA_TOKEN") {
- try await recaptchaVerifier.injectRecaptchaFields(
- request: request,
- provider: AuthRecaptchaProvider.password,
- action: action
- )
- } else {
- throw error
- }
- }
- }
- return try await AuthBackend.call(with: request)
- }
- #endif
- private func completeSignIn(withAccessToken accessToken: String?,
- accessTokenExpirationDate: Date?,
- refreshToken: String?,
- anonymous: Bool) async throws -> User {
- return try await User.retrieveUser(withAuth: requestConfiguration.auth!,
- accessToken: accessToken,
- accessTokenExpirationDate: accessTokenExpirationDate,
- refreshToken: refreshToken,
- anonymous: anonymous)
- }
- init(requestConfiguration: AuthRequestConfiguration) {
- self.requestConfiguration = requestConfiguration
- }
- }
|