AuthWorker.swift 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226
  1. // Copyright 2024 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. #if COCOAPODS
  16. @_implementationOnly import GoogleUtilities
  17. #else
  18. @_implementationOnly import GoogleUtilities_AppDelegateSwizzler
  19. @_implementationOnly import GoogleUtilities_Environment
  20. #endif
  21. #if canImport(UIKit)
  22. import UIKit
  23. #endif
  24. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  25. actor AuthWorker {
  26. let requestConfiguration: AuthRequestConfiguration
  27. func getLanguageCode() -> String? {
  28. return requestConfiguration.languageCode
  29. }
  30. func setLanguageCode(_ code: String?) {
  31. requestConfiguration.languageCode = code
  32. }
  33. /// The manager for APNs tokens used by phone number auth.
  34. var tokenManager: AuthAPNSTokenManager!
  35. func tokenManagerCancel(error: Error) {
  36. tokenManager.cancel(withError: error)
  37. }
  38. func tokenManagerSet(_ token: Data, type: AuthAPNSTokenType) {
  39. tokenManager.token = AuthAPNSToken(withData: token, type: type)
  40. }
  41. func tokenManagerGet() -> AuthAPNSTokenManager {
  42. return tokenManager
  43. }
  44. func getToken(forcingRefresh forceRefresh: Bool) async throws -> String? {
  45. // Enable token auto-refresh if not already enabled.
  46. guard let auth = requestConfiguration.auth else {
  47. return nil
  48. }
  49. auth.getTokenInternal(forcingRefresh: forceRefresh)
  50. // Call back with 'nil' if there is no current user.
  51. guard let currentUser = auth.currentUser else {
  52. return nil
  53. }
  54. return try await currentUser.internalGetToken(forceRefresh: forceRefresh)
  55. }
  56. /// Only for testing
  57. func tokenManagerInit(_ manager: AuthAPNSTokenManager) {
  58. tokenManager = manager
  59. }
  60. func fetchSignInMethods(forEmail email: String) async throws -> [String] {
  61. let request = CreateAuthURIRequest(identifier: email,
  62. continueURI: "http:www.google.com",
  63. requestConfiguration: requestConfiguration)
  64. let response = try await AuthBackend.call(with: request)
  65. return response.signinMethods ?? []
  66. }
  67. func signIn(withEmail email: String, password: String) async throws -> AuthDataResult {
  68. let credential = EmailAuthCredential(withEmail: email, password: password)
  69. return try await internalSignInAndRetrieveData(withCredential: credential,
  70. isReauthentication: false)
  71. }
  72. func signIn(withEmail email: String, link: String) async throws -> AuthDataResult {
  73. let credential = EmailAuthCredential(withEmail: email, link: link)
  74. return try await internalSignInAndRetrieveData(withCredential: credential,
  75. isReauthentication: false)
  76. }
  77. func signIn(with credential: AuthCredential) async throws -> AuthDataResult {
  78. return try await internalSignInAndRetrieveData(withCredential: credential,
  79. isReauthentication: false)
  80. }
  81. #if os(iOS)
  82. func signIn(with provider: FederatedAuthProvider,
  83. uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
  84. let credential = try await provider.credential(with: uiDelegate)
  85. return try await internalSignInAndRetrieveData(
  86. withCredential: credential,
  87. isReauthentication: false
  88. )
  89. }
  90. #endif
  91. func signInAnonymously() async throws -> AuthDataResult {
  92. if let currentUser = requestConfiguration.auth?.currentUser,
  93. currentUser.isAnonymous {
  94. return AuthDataResult(withUser: currentUser, additionalUserInfo: nil)
  95. }
  96. let request = SignUpNewUserRequest(requestConfiguration: requestConfiguration)
  97. let response = try await AuthBackend.call(with: request)
  98. let user = try await completeSignIn(
  99. withAccessToken: response.idToken,
  100. accessTokenExpirationDate: response.approximateExpirationDate,
  101. refreshToken: response.refreshToken,
  102. anonymous: true
  103. )
  104. // TODO: The ObjC implementation passed a nil providerID to the nonnull providerID
  105. let additionalUserInfo = AdditionalUserInfo(providerID: "",
  106. profile: nil,
  107. username: nil,
  108. isNewUser: true)
  109. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  110. }
  111. func signIn(withCustomToken token: String) async throws -> AuthDataResult {
  112. let request = VerifyCustomTokenRequest(token: token,
  113. requestConfiguration: requestConfiguration)
  114. let response = try await AuthBackend.call(with: request)
  115. let user = try await completeSignIn(
  116. withAccessToken: response.idToken,
  117. accessTokenExpirationDate: response.approximateExpirationDate,
  118. refreshToken: response.refreshToken,
  119. anonymous: false
  120. )
  121. // TODO: The ObjC implementation passed a nil providerID to the nonnull providerID
  122. let additionalUserInfo = AdditionalUserInfo(providerID: "",
  123. profile: nil,
  124. username: nil,
  125. isNewUser: response.isNewUser)
  126. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  127. }
  128. func createUser(withEmail email: String, password: String) async throws -> AuthDataResult {
  129. let request = SignUpNewUserRequest(email: email,
  130. password: password,
  131. displayName: nil,
  132. idToken: nil,
  133. requestConfiguration: requestConfiguration)
  134. #if os(iOS)
  135. let response = try await injectRecaptcha(request: request,
  136. action: AuthRecaptchaAction.signUpPassword)
  137. #else
  138. let response = try await AuthBackend.call(with: request)
  139. #endif
  140. let user = try await completeSignIn(
  141. withAccessToken: response.idToken,
  142. accessTokenExpirationDate: response.approximateExpirationDate,
  143. refreshToken: response.refreshToken,
  144. anonymous: false
  145. )
  146. let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
  147. profile: nil,
  148. username: nil,
  149. isNewUser: true)
  150. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  151. }
  152. func confirmPasswordReset(withCode code: String, newPassword: String) async throws {
  153. let request = ResetPasswordRequest(oobCode: code,
  154. newPassword: newPassword,
  155. requestConfiguration: requestConfiguration)
  156. _ = try await AuthBackend.call(with: request)
  157. }
  158. func checkActionCode(_ code: String) async throws -> ActionCodeInfo {
  159. let request = ResetPasswordRequest(oobCode: code,
  160. newPassword: nil,
  161. requestConfiguration: requestConfiguration)
  162. let response = try await AuthBackend.call(with: request)
  163. let operation = ActionCodeInfo.actionCodeOperation(forRequestType: response.requestType)
  164. guard let email = response.email else {
  165. fatalError("Internal Auth Error: Failed to get a ResetPasswordResponse")
  166. }
  167. return ActionCodeInfo(withOperation: operation,
  168. email: email,
  169. newEmail: response.verifiedEmail)
  170. }
  171. func verifyPasswordResetCode(_ code: String) async throws -> String {
  172. let info = try await checkActionCode(code)
  173. return info.email
  174. }
  175. func applyActionCode(_ code: String) async throws {
  176. let request = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
  177. request.oobCode = code
  178. _ = try await AuthBackend.call(with: request)
  179. }
  180. func sendPasswordReset(withEmail email: String,
  181. actionCodeSettings: ActionCodeSettings? = nil) async throws {
  182. let request = GetOOBConfirmationCodeRequest.passwordResetRequest(
  183. email: email,
  184. actionCodeSettings: actionCodeSettings,
  185. requestConfiguration: requestConfiguration
  186. )
  187. #if os(iOS)
  188. _ = try await injectRecaptcha(request: request,
  189. action: AuthRecaptchaAction.getOobCode)
  190. #else
  191. _ = try await AuthBackend.call(with: request)
  192. #endif
  193. }
  194. func sendSignInLink(toEmail email: String,
  195. actionCodeSettings: ActionCodeSettings) async throws {
  196. let request = GetOOBConfirmationCodeRequest.signInWithEmailLinkRequest(
  197. email,
  198. actionCodeSettings: actionCodeSettings,
  199. requestConfiguration: requestConfiguration
  200. )
  201. #if os(iOS)
  202. _ = try await injectRecaptcha(request: request,
  203. action: AuthRecaptchaAction.getOobCode)
  204. #else
  205. _ = try await AuthBackend.call(with: request)
  206. #endif
  207. }
  208. func signOut() throws {
  209. guard requestConfiguration.auth?.currentUser != nil else {
  210. return
  211. }
  212. try updateCurrentUser(nil, byForce: false, savingToDisk: true)
  213. }
  214. func updateCurrentUser(_ user: User) async throws {
  215. if user.requestConfiguration.apiKey != requestConfiguration.apiKey {
  216. // If the API keys are different, then we need to confirm that the user belongs to the same
  217. // project before proceeding.
  218. user.requestConfiguration = requestConfiguration
  219. try await user.reload()
  220. }
  221. try updateCurrentUser(user, byForce: true, savingToDisk: true)
  222. }
  223. /// Continue with the rest of the Auth object initialization in the worker actor.
  224. func protectedDataInitialization(_ keychainStorageProvider: AuthKeychainStorage) {
  225. // Load current user from Keychain.
  226. guard let auth = requestConfiguration.auth else {
  227. return
  228. }
  229. if let keychainServiceName = Auth.keychainServiceName(forAppName: auth.firebaseAppName) {
  230. auth.keychainServices = AuthKeychainServices(service: keychainServiceName,
  231. storage: keychainStorageProvider)
  232. auth.storedUserManager = AuthStoredUserManager(
  233. serviceName: keychainServiceName,
  234. keychainServices: auth.keychainServices
  235. )
  236. }
  237. do {
  238. if let storedUserAccessGroup = auth.storedUserManager.getStoredUserAccessGroup() {
  239. try auth.internalUseUserAccessGroup(storedUserAccessGroup)
  240. } else {
  241. let user = try auth.getUser()
  242. try updateCurrentUser(user, byForce: false, savingToDisk: false)
  243. if let user {
  244. auth.tenantID = user.tenantID
  245. auth.lastNotifiedUserToken = user.rawAccessToken()
  246. }
  247. }
  248. } catch {
  249. #if canImport(UIKit)
  250. if (error as NSError).code == AuthErrorCode.keychainError.rawValue {
  251. // If there's a keychain error, assume it is due to the keychain being accessed
  252. // before the device is unlocked as a result of prewarming, and listen for the
  253. // UIApplicationProtectedDataDidBecomeAvailable notification.
  254. auth.addProtectedDataDidBecomeAvailableObserver()
  255. }
  256. #endif
  257. AuthLog.logError(code: "I-AUT000001",
  258. message: "Error loading saved user when starting up: \(error)")
  259. }
  260. #if os(iOS)
  261. if GULAppEnvironmentUtil.isAppExtension() {
  262. // iOS App extensions should not call [UIApplication sharedApplication], even if
  263. // UIApplication responds to it.
  264. return
  265. }
  266. // Using reflection here to avoid build errors in extensions.
  267. let sel = NSSelectorFromString("sharedApplication")
  268. guard UIApplication.responds(to: sel),
  269. let rawApplication = UIApplication.perform(sel),
  270. let application = rawApplication.takeUnretainedValue() as? UIApplication else {
  271. return
  272. }
  273. // Initialize for phone number auth.
  274. tokenManager = AuthAPNSTokenManager(withApplication: application)
  275. auth.appCredentialManager = AuthAppCredentialManager(withKeychain: auth.keychainServices)
  276. auth.notificationManager = AuthNotificationManager(
  277. withApplication: application,
  278. appCredentialManager: auth.appCredentialManager
  279. )
  280. GULAppDelegateSwizzler.registerAppDelegateInterceptor(auth)
  281. GULSceneDelegateSwizzler.registerSceneDelegateInterceptor(auth)
  282. #endif
  283. }
  284. // MARK: User.swift implementations
  285. func updateEmail(user: User,
  286. email: String?,
  287. password: String?) async throws {
  288. let hadEmailPasswordCredential = user.hasEmailPasswordCredential
  289. try await executeUserUpdateWithChanges(user: user) { userAccount, request in
  290. if let email {
  291. request.email = email
  292. }
  293. if let password {
  294. request.password = password
  295. }
  296. }
  297. if let email {
  298. user.email = email
  299. }
  300. if user.email != nil {
  301. guard !hadEmailPasswordCredential else {
  302. if let error = user.updateKeychain() {
  303. throw error
  304. }
  305. return
  306. }
  307. // The list of providers need to be updated for the newly added email-password provider.
  308. let accessToken = try await user.internalGetToken()
  309. let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken,
  310. requestConfiguration: requestConfiguration)
  311. do {
  312. let accountInfoResponse = try await AuthBackend.call(with: getAccountInfoRequest)
  313. if let users = accountInfoResponse.users {
  314. for userAccountInfo in users {
  315. // Set the account to non-anonymous if there are any providers, even if
  316. // they're not email/password ones.
  317. if let providerUsers = userAccountInfo.providerUserInfo {
  318. if providerUsers.count > 0 {
  319. user.isAnonymous = false
  320. for providerUserInfo in providerUsers {
  321. if providerUserInfo.providerID == EmailAuthProvider.id {
  322. user.hasEmailPasswordCredential = true
  323. break
  324. }
  325. }
  326. }
  327. }
  328. }
  329. }
  330. user.update(withGetAccountInfoResponse: accountInfoResponse)
  331. if let error = user.updateKeychain() {
  332. throw error
  333. }
  334. } catch {
  335. user.signOutIfTokenIsInvalid(withError: error)
  336. throw error
  337. }
  338. }
  339. }
  340. #if os(iOS)
  341. /// Updates the phone number for the user. On success, the cached user profile data is updated.
  342. ///
  343. /// Invoked asynchronously on the global work queue in the future.
  344. /// - Parameter credential: The new phone number credential corresponding to the phone
  345. /// number to be added to the Firebase account. If a phone number is already linked to the
  346. /// account, this new phone number will replace it.
  347. /// - Parameter isLinkOperation: Boolean value indicating whether or not this is a link
  348. /// operation.
  349. func updateOrLinkPhoneNumber(user: User, credential: PhoneAuthCredential,
  350. isLinkOperation: Bool) async throws {
  351. let accessToken = try await user.internalGetToken()
  352. guard let configuration = user.auth?.requestConfiguration else {
  353. fatalError("Auth Internal Error: nil value for VerifyPhoneNumberRequest initializer")
  354. }
  355. switch credential.credentialKind {
  356. case .phoneNumber: fatalError("Internal Error: Missing verificationCode")
  357. case let .verification(verificationID, code):
  358. let operation = isLinkOperation ? AuthOperationType.link : AuthOperationType.update
  359. let request = VerifyPhoneNumberRequest(verificationID: verificationID,
  360. verificationCode: code,
  361. operation: operation,
  362. requestConfiguration: configuration)
  363. request.accessToken = accessToken
  364. do {
  365. let verifyResponse = try await AuthBackend.call(with: request)
  366. guard let idToken = verifyResponse.idToken,
  367. let refreshToken = verifyResponse.refreshToken else {
  368. fatalError("Internal Auth Error: missing token in internalUpdateOrLinkPhoneNumber")
  369. }
  370. user.tokenService = SecureTokenService(
  371. withRequestConfiguration: configuration,
  372. accessToken: idToken,
  373. accessTokenExpirationDate: verifyResponse.approximateExpirationDate,
  374. refreshToken: refreshToken
  375. )
  376. // Get account info to update cached user info.
  377. _ = try await getAccountInfoRefreshingCache(user)
  378. user.isAnonymous = false
  379. if let error = user.updateKeychain() {
  380. throw error
  381. }
  382. } catch {
  383. user.signOutIfTokenIsInvalid(withError: error)
  384. throw error
  385. }
  386. }
  387. }
  388. #endif
  389. /// Performs a setAccountInfo request by mutating the results of a getAccountInfo response,
  390. /// atomically in regards to other calls to this method.
  391. /// - Parameter changeBlock: A block responsible for mutating a template `SetAccountInfoRequest`
  392. /// - Parameter callback: A block to invoke when the change is complete. Invoked asynchronously on
  393. /// the auth global work queue in the future.
  394. private func executeUserUpdateWithChanges(user: User,
  395. changeBlock: @escaping (GetAccountInfoResponseUser,
  396. SetAccountInfoRequest)
  397. -> Void) async throws {
  398. let userAccountInfo = try await getAccountInfoRefreshingCache(user)
  399. let accessToken = try await user.internalGetToken()
  400. // Mutate setAccountInfoRequest in block
  401. let setAccountInfoRequest = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
  402. setAccountInfoRequest.accessToken = accessToken
  403. changeBlock(userAccountInfo, setAccountInfoRequest)
  404. do {
  405. let accountInfoResponse = try await AuthBackend.call(with: setAccountInfoRequest)
  406. if let idToken = accountInfoResponse.idToken,
  407. let refreshToken = accountInfoResponse.refreshToken {
  408. let tokenService = SecureTokenService(
  409. withRequestConfiguration: requestConfiguration,
  410. accessToken: idToken,
  411. accessTokenExpirationDate: accountInfoResponse.approximateExpirationDate,
  412. refreshToken: refreshToken
  413. )
  414. try await user.setTokenService(tokenService: tokenService)
  415. }
  416. } catch {
  417. user.signOutIfTokenIsInvalid(withError: error)
  418. throw error
  419. }
  420. }
  421. /// Gets the users' account data from the server, updating our local values.
  422. /// - Parameter callback: Invoked when the request to getAccountInfo has completed, or when an
  423. /// error has been detected. Invoked asynchronously on the auth global work queue in the future.
  424. func getAccountInfoRefreshingCache(_ user: User) async throws
  425. -> GetAccountInfoResponseUser {
  426. let token = try await user.internalGetToken()
  427. let request = GetAccountInfoRequest(accessToken: token,
  428. requestConfiguration: requestConfiguration)
  429. do {
  430. let accountInfoResponse = try await AuthBackend.call(with: request)
  431. user.update(withGetAccountInfoResponse: accountInfoResponse)
  432. if let error = user.updateKeychain() {
  433. throw error
  434. }
  435. return (accountInfoResponse.users?.first)!
  436. } catch {
  437. user.signOutIfTokenIsInvalid(withError: error)
  438. throw error
  439. }
  440. }
  441. func reauthenticate(with credential: AuthCredential) async throws -> AuthDataResult {
  442. do {
  443. let authResult = try await internalSignInAndRetrieveData(
  444. withCredential: credential,
  445. isReauthentication: true
  446. )
  447. let user = authResult.user
  448. guard user.uid == requestConfiguration.auth?.getUserID() else {
  449. throw AuthErrorUtils.userMismatchError()
  450. }
  451. try await user.setTokenService(tokenService: user.tokenService)
  452. return authResult
  453. } catch {
  454. if (error as NSError).code == AuthErrorCode.userNotFound.rawValue {
  455. throw AuthErrorUtils.userMismatchError()
  456. }
  457. throw error
  458. }
  459. }
  460. #if os(iOS)
  461. func reauthenticate(with provider: FederatedAuthProvider,
  462. uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
  463. let credential = try await provider.credential(with: uiDelegate)
  464. return try await reauthenticate(with: credential)
  465. }
  466. #endif
  467. func getIDTokenResult(user: User,
  468. forcingRefresh forceRefresh: Bool) async throws -> AuthTokenResult {
  469. let token = try await user.internalGetToken(forceRefresh: forceRefresh)
  470. let tokenResult = try AuthTokenResult.tokenResult(token: token)
  471. AuthLog.logDebug(code: "I-AUT000017", message: "Actual token expiration date: " +
  472. "\(String(describing: tokenResult.expirationDate))," +
  473. "current date: \(Date())")
  474. return tokenResult
  475. }
  476. func link(user: User, with credential: AuthCredential) async throws -> AuthDataResult {
  477. if user.providerDataRaw[credential.provider] != nil {
  478. throw AuthErrorUtils.providerAlreadyLinkedError()
  479. }
  480. if let emailCredential = credential as? EmailAuthCredential {
  481. return try await link(user: user, withEmailCredential: emailCredential)
  482. }
  483. #if !os(watchOS)
  484. if let gameCenterCredential = credential as? GameCenterAuthCredential {
  485. return try await link(user: user, withGameCenterCredential: gameCenterCredential)
  486. }
  487. #endif
  488. #if os(iOS)
  489. if let phoneCredential = credential as? PhoneAuthCredential {
  490. return try await link(user: user, withPhoneCredential: phoneCredential)
  491. }
  492. #endif
  493. let accessToken = try await user.internalGetToken()
  494. let request = VerifyAssertionRequest(providerID: credential.provider,
  495. requestConfiguration: requestConfiguration)
  496. credential.prepare(request)
  497. request.accessToken = accessToken
  498. do {
  499. let response = try await AuthBackend.call(with: request)
  500. guard let idToken = response.idToken,
  501. let refreshToken = response.refreshToken,
  502. let providerID = response.providerID else {
  503. fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
  504. }
  505. try await updateTokenAndRefreshUser(user: user,
  506. idToken: idToken,
  507. refreshToken: refreshToken,
  508. expirationDate: response.approximateExpirationDate)
  509. let updatedOAuthCredential = OAuthCredential(withVerifyAssertionResponse: response)
  510. let additionalUserInfo = AdditionalUserInfo(providerID: providerID,
  511. profile: response.profile,
  512. username: response.username,
  513. isNewUser: response.isNewUser)
  514. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo,
  515. credential: updatedOAuthCredential)
  516. } catch {
  517. user.signOutIfTokenIsInvalid(withError: error)
  518. throw error
  519. }
  520. }
  521. func link(user: User, with provider: FederatedAuthProvider,
  522. uiDelegate: AuthUIDelegate?) async throws -> AuthDataResult {
  523. let credential = try await provider.credential(with: uiDelegate)
  524. return try await link(user: user, with: credential)
  525. }
  526. func unlink(user: User, fromProvider provider: String) async throws -> User {
  527. let accessToken = try await user.internalGetToken()
  528. let request = SetAccountInfoRequest(requestConfiguration: requestConfiguration)
  529. request.accessToken = accessToken
  530. if user.providerDataRaw[provider] == nil {
  531. throw AuthErrorUtils.noSuchProviderError()
  532. }
  533. request.deleteProviders = [provider]
  534. do {
  535. let response = try await AuthBackend.call(with: request)
  536. // We can't just use the provider info objects in SetAccountInfoResponse
  537. // because they don't have localID and email fields. Remove the specific
  538. // provider manually.
  539. user.providerDataRaw.removeValue(forKey: provider)
  540. if provider == EmailAuthProvider.id {
  541. user.hasEmailPasswordCredential = false
  542. }
  543. #if os(iOS)
  544. // After successfully unlinking a phone auth provider, remove the phone number
  545. // from the cached user info.
  546. if provider == PhoneAuthProvider.id {
  547. user.phoneNumber = nil
  548. }
  549. #endif
  550. if let idToken = response.idToken,
  551. let refreshToken = response.refreshToken {
  552. let tokenService = SecureTokenService(
  553. withRequestConfiguration: requestConfiguration,
  554. accessToken: idToken,
  555. accessTokenExpirationDate: response.approximateExpirationDate,
  556. refreshToken: refreshToken
  557. )
  558. try await user.setTokenService(tokenService: tokenService)
  559. return user
  560. }
  561. } catch {
  562. user.signOutIfTokenIsInvalid(withError: error)
  563. throw error
  564. }
  565. if let error = user.updateKeychain() {
  566. throw error
  567. }
  568. return user
  569. }
  570. func sendEmailVerification(user: User,
  571. with actionCodeSettings: ActionCodeSettings?) async throws {
  572. let accessToken = try await user.internalGetToken()
  573. let request = GetOOBConfirmationCodeRequest.verifyEmailRequest(
  574. accessToken: accessToken,
  575. actionCodeSettings: actionCodeSettings,
  576. requestConfiguration: requestConfiguration
  577. )
  578. do {
  579. _ = try await AuthBackend.call(with: request)
  580. } catch {
  581. user.signOutIfTokenIsInvalid(withError: error)
  582. throw error
  583. }
  584. }
  585. func sendEmailVerification(user: User,
  586. beforeUpdatingEmail newEmail: String,
  587. actionCodeSettings: ActionCodeSettings?) async throws {
  588. let accessToken = try await user.internalGetToken()
  589. let request = GetOOBConfirmationCodeRequest.verifyBeforeUpdateEmail(
  590. accessToken: accessToken,
  591. newEmail: newEmail,
  592. actionCodeSettings: actionCodeSettings,
  593. requestConfiguration: requestConfiguration
  594. )
  595. do {
  596. _ = try await AuthBackend.call(with: request)
  597. } catch {
  598. user.signOutIfTokenIsInvalid(withError: error)
  599. throw error
  600. }
  601. }
  602. func delete(user: User) async throws {
  603. let accessToken = try await user.internalGetToken()
  604. let request = DeleteAccountRequest(localID: user.uid, accessToken: accessToken,
  605. requestConfiguration: requestConfiguration)
  606. _ = try await AuthBackend.call(with: request)
  607. try user.auth?.signOutByForce(withUserID: user.uid)
  608. }
  609. func commitChanges(changeRequest: UserProfileChangeRequest) async throws {
  610. if changeRequest.consumed {
  611. fatalError("Internal Auth Error: commitChanges should only be called once.")
  612. }
  613. changeRequest.consumed = true
  614. // Return fast if there is nothing to update:
  615. if !changeRequest.photoURLWasSet, !changeRequest.displayNameWasSet {
  616. return
  617. }
  618. let displayName = changeRequest.displayName
  619. let displayNameWasSet = changeRequest.displayNameWasSet
  620. let photoURL = changeRequest.photoURL
  621. let photoURLWasSet = changeRequest.photoURLWasSet
  622. try await executeUserUpdateWithChanges(user: changeRequest.user) { _, request in
  623. if photoURLWasSet {
  624. request.photoURL = photoURL
  625. }
  626. if displayNameWasSet {
  627. request.displayName = displayName
  628. }
  629. }
  630. if displayNameWasSet {
  631. changeRequest.user.displayName = displayName
  632. }
  633. if photoURLWasSet {
  634. changeRequest.user.photoURL = photoURL
  635. }
  636. if let error = changeRequest.user.updateKeychain() {
  637. throw error
  638. }
  639. }
  640. private func link(user: User,
  641. withEmailCredential emailCredential: EmailAuthCredential) async throws
  642. -> AuthDataResult {
  643. if user.hasEmailPasswordCredential {
  644. throw AuthErrorUtils.providerAlreadyLinkedError()
  645. }
  646. switch emailCredential.emailType {
  647. case let .password(password):
  648. let result = AuthDataResult(withUser: user, additionalUserInfo: nil)
  649. return try await link(
  650. user: user,
  651. withEmail: emailCredential.email,
  652. password: password,
  653. authResult: result
  654. )
  655. case let .link(link):
  656. let accessToken = try? await user.internalGetToken()
  657. var queryItems = AuthWebUtils.parseURL(link)
  658. if link.count == 0 {
  659. if let urlComponents = URLComponents(string: link),
  660. let query = urlComponents.query {
  661. queryItems = AuthWebUtils.parseURL(query)
  662. }
  663. }
  664. guard let actionCode = queryItems["oobCode"] else {
  665. fatalError("Internal Auth Error: Missing oobCode")
  666. }
  667. let request = EmailLinkSignInRequest(email: emailCredential.email,
  668. oobCode: actionCode,
  669. requestConfiguration: requestConfiguration)
  670. request.idToken = accessToken
  671. let response = try await AuthBackend.call(with: request)
  672. guard let idToken = response.idToken,
  673. let refreshToken = response.refreshToken else {
  674. fatalError("Internal Auth Error: missing token in EmailLinkSignInResponse")
  675. }
  676. try await updateTokenAndRefreshUser(
  677. user: user,
  678. idToken: idToken,
  679. refreshToken: refreshToken,
  680. expirationDate: response.approximateExpirationDate
  681. )
  682. return AuthDataResult(withUser: user, additionalUserInfo: nil)
  683. }
  684. }
  685. private func link(user: User,
  686. withEmail email: String,
  687. password: String,
  688. authResult: AuthDataResult) async throws -> AuthDataResult {
  689. let accessToken = try await user.internalGetToken()
  690. do {
  691. let request = SignUpNewUserRequest(email: email,
  692. password: password,
  693. displayName: nil,
  694. idToken: accessToken,
  695. requestConfiguration: requestConfiguration)
  696. #if os(iOS)
  697. let response = try await injectRecaptcha(request: request,
  698. action: AuthRecaptchaAction.signUpPassword)
  699. #else
  700. let response = try await AuthBackend.call(with: request)
  701. #endif
  702. guard let refreshToken = response.refreshToken,
  703. let idToken = response.idToken else {
  704. fatalError("Internal auth error: Invalid SignUpNewUserResponse")
  705. }
  706. // Update the new token and refresh user info again.
  707. user.tokenService = SecureTokenService(
  708. withRequestConfiguration: requestConfiguration,
  709. accessToken: idToken,
  710. accessTokenExpirationDate: response.approximateExpirationDate,
  711. refreshToken: refreshToken
  712. )
  713. let accessToken = try await user.internalGetToken()
  714. let getAccountInfoRequest = GetAccountInfoRequest(
  715. accessToken: accessToken,
  716. requestConfiguration: requestConfiguration
  717. )
  718. let accountResponse = try await AuthBackend.call(with: getAccountInfoRequest)
  719. user.isAnonymous = false
  720. user.update(withGetAccountInfoResponse: accountResponse)
  721. if let keychainError = user.updateKeychain() {
  722. throw keychainError
  723. }
  724. return authResult
  725. } catch {
  726. user.signOutIfTokenIsInvalid(withError: error)
  727. throw error
  728. }
  729. }
  730. #if !os(watchOS)
  731. private func link(user: User,
  732. withGameCenterCredential gameCenterCredential: GameCenterAuthCredential) async throws
  733. -> AuthDataResult {
  734. let accessToken = try await user.internalGetToken()
  735. guard let publicKeyURL = gameCenterCredential.publicKeyURL,
  736. let signature = gameCenterCredential.signature,
  737. let salt = gameCenterCredential.salt else {
  738. fatalError("Internal Auth Error: Nil value field for SignInWithGameCenterRequest")
  739. }
  740. let request = SignInWithGameCenterRequest(playerID: gameCenterCredential.playerID,
  741. teamPlayerID: gameCenterCredential.teamPlayerID,
  742. gamePlayerID: gameCenterCredential.gamePlayerID,
  743. publicKeyURL: publicKeyURL,
  744. signature: signature,
  745. salt: salt,
  746. timestamp: gameCenterCredential.timestamp,
  747. displayName: gameCenterCredential.displayName,
  748. requestConfiguration: requestConfiguration)
  749. request.accessToken = accessToken
  750. let response = try await AuthBackend.call(with: request)
  751. guard let idToken = response.idToken,
  752. let refreshToken = response.refreshToken else {
  753. fatalError("Internal Auth Error: missing token in link(withGameCredential")
  754. }
  755. try await updateTokenAndRefreshUser(user: user,
  756. idToken: idToken,
  757. refreshToken: refreshToken,
  758. expirationDate: response.approximateExpirationDate)
  759. return AuthDataResult(withUser: user, additionalUserInfo: nil)
  760. }
  761. #endif
  762. #if os(iOS)
  763. private func link(user: User,
  764. withPhoneCredential phoneCredential: PhoneAuthCredential) async throws
  765. -> AuthDataResult {
  766. try await updateOrLinkPhoneNumber(user: user,
  767. credential: phoneCredential,
  768. isLinkOperation: true)
  769. return AuthDataResult(withUser: user, additionalUserInfo: nil)
  770. }
  771. #endif
  772. // Update the new token and refresh user info again.
  773. private func updateTokenAndRefreshUser(user: User,
  774. idToken: String,
  775. refreshToken: String,
  776. expirationDate: Date?) async throws {
  777. user.tokenService = SecureTokenService(
  778. withRequestConfiguration: requestConfiguration,
  779. accessToken: idToken,
  780. accessTokenExpirationDate: expirationDate,
  781. refreshToken: refreshToken
  782. )
  783. let accessToken = try await user.internalGetToken()
  784. let getAccountInfoRequest = GetAccountInfoRequest(accessToken: accessToken,
  785. requestConfiguration: requestConfiguration)
  786. do {
  787. let response = try await AuthBackend.call(with: getAccountInfoRequest)
  788. user.isAnonymous = false
  789. user.update(withGetAccountInfoResponse: response)
  790. } catch {
  791. user.signOutIfTokenIsInvalid(withError: error)
  792. throw error
  793. }
  794. if let error = user.updateKeychain() {
  795. throw error
  796. }
  797. }
  798. /// Update the current user; initializing the user's internal properties correctly, and
  799. /// optionally saving the user to disk.
  800. ///
  801. /// This method is called during: sign in and sign out events, as well as during class
  802. /// initialization time. The only time the saveToDisk parameter should be set to NO is during
  803. /// class initialization time because the user was just read from disk.
  804. /// - Parameter user: The user to use as the current user (including nil, which is passed at sign
  805. /// out time.)
  806. /// - Parameter saveToDisk: Indicates the method should persist the user data to disk.
  807. func updateCurrentUser(_ user: User?, byForce force: Bool,
  808. savingToDisk saveToDisk: Bool) throws {
  809. if user == requestConfiguration.auth?.currentUser {
  810. // TODO: local
  811. requestConfiguration.auth?.possiblyPostAuthStateChangeNotification()
  812. }
  813. if let user {
  814. if user.tenantID != requestConfiguration.auth?.tenantID {
  815. let error = AuthErrorUtils.tenantIDMismatchError()
  816. throw error
  817. }
  818. }
  819. var throwError: Error?
  820. if saveToDisk {
  821. do {
  822. // TODO: call local saveSuer
  823. try requestConfiguration.auth?.saveUser(user)
  824. } catch {
  825. throwError = error
  826. }
  827. }
  828. if throwError == nil || force {
  829. requestConfiguration.auth?.currentUser = user
  830. // TODO:
  831. requestConfiguration.auth?.possiblyPostAuthStateChangeNotification()
  832. }
  833. if let throwError {
  834. throw throwError
  835. }
  836. }
  837. func useEmulator(withHost host: String, port: Int) async {
  838. // If host is an IPv6 address, it should be formatted with surrounding brackets.
  839. let formattedHost = host.contains(":") ? "[\(host)]" : host
  840. requestConfiguration.emulatorHostAndPort = "\(formattedHost):\(port)"
  841. #if os(iOS)
  842. requestConfiguration.auth?.settings?.appVerificationDisabledForTesting = true
  843. #endif
  844. }
  845. #if os(iOS)
  846. func canHandleNotification(_ userInfo: [AnyHashable: Any]) async -> Bool {
  847. guard let auth = requestConfiguration.auth else {
  848. return false
  849. }
  850. return auth.notificationManager.canHandle(notification: userInfo)
  851. }
  852. func canHandle(_ url: URL) -> Bool {
  853. guard let auth = requestConfiguration.auth,
  854. let authURLPresenter = auth.authURLPresenter as? AuthURLPresenter else {
  855. return false
  856. }
  857. return authURLPresenter.canHandle(url: url)
  858. }
  859. #endif
  860. func autoTokenRefresh(accessToken: String, retry: Bool, delay: TimeInterval) async {
  861. try? await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
  862. guard let auth = requestConfiguration.auth,
  863. let currentUser = auth.currentUser else {
  864. return
  865. }
  866. let accessToken = currentUser.rawAccessToken()
  867. guard currentUser.rawAccessToken() == accessToken else {
  868. // Another auto refresh must have been scheduled, so keep _autoRefreshScheduled unchanged.
  869. return
  870. }
  871. auth.autoRefreshScheduled = false
  872. if auth.isAppInBackground {
  873. return
  874. }
  875. let uid = currentUser.uid
  876. do {
  877. _ = try await currentUser.internalGetToken(forceRefresh: true)
  878. if auth.currentUser?.uid != uid {
  879. return
  880. }
  881. } catch {
  882. // Kicks off exponential back off logic to retry failed attempt. Starts with one minute
  883. // delay (60 seconds) if this is the first failed attempt.
  884. let rescheduleDelay = retry ? min(delay * 2, 16 * 60) : 60
  885. auth.scheduleAutoTokenRefresh(withDelay: rescheduleDelay, retry: true)
  886. }
  887. }
  888. func fetchAccessToken(user: User,
  889. forcingRefresh forceRefresh: Bool) async throws -> (String?, Bool) {
  890. if !forceRefresh, user.tokenService.hasValidAccessToken() {
  891. return (user.tokenService.accessToken, false)
  892. } else {
  893. AuthLog.logDebug(code: "I-AUT000017", message: "Fetching new token from backend.")
  894. return try await user.tokenService.requestAccessToken(retryIfExpired: true)
  895. }
  896. }
  897. private func internalSignInAndRetrieveData(withCredential credential: AuthCredential,
  898. isReauthentication: Bool) async throws
  899. -> AuthDataResult {
  900. if let emailCredential = credential as? EmailAuthCredential {
  901. // Special case for email/password credentials
  902. switch emailCredential.emailType {
  903. case let .link(link):
  904. // Email link sign in
  905. return try await internalSignInAndRetrieveData(withEmail: emailCredential.email, link: link)
  906. case let .password(password):
  907. // Email password sign in
  908. let user = try await internalSignInUser(
  909. withEmail: emailCredential.email,
  910. password: password
  911. )
  912. let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
  913. profile: nil,
  914. username: nil,
  915. isNewUser: false)
  916. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  917. }
  918. }
  919. #if !os(watchOS)
  920. if let gameCenterCredential = credential as? GameCenterAuthCredential {
  921. return try await signInAndRetrieveData(withGameCenterCredential: gameCenterCredential)
  922. }
  923. #endif
  924. #if os(iOS)
  925. if let phoneCredential = credential as? PhoneAuthCredential {
  926. // Special case for phone auth credentials
  927. let operation = isReauthentication ? AuthOperationType.reauth :
  928. AuthOperationType.signUpOrSignIn
  929. let response = try await signIn(withPhoneCredential: phoneCredential,
  930. operation: operation)
  931. let user = try await completeSignIn(withAccessToken: response.idToken,
  932. accessTokenExpirationDate: response
  933. .approximateExpirationDate,
  934. refreshToken: response.refreshToken,
  935. anonymous: false)
  936. let additionalUserInfo = AdditionalUserInfo(providerID: PhoneAuthProvider.id,
  937. profile: nil,
  938. username: nil,
  939. isNewUser: response.isNewUser)
  940. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  941. }
  942. #endif
  943. let request = VerifyAssertionRequest(providerID: credential.provider,
  944. requestConfiguration: requestConfiguration)
  945. request.autoCreate = !isReauthentication
  946. credential.prepare(request)
  947. let response = try await AuthBackend.call(with: request)
  948. if response.needConfirmation {
  949. let email = response.email
  950. let credential = OAuthCredential(withVerifyAssertionResponse: response)
  951. throw AuthErrorUtils.accountExistsWithDifferentCredentialError(
  952. email: email,
  953. updatedCredential: credential
  954. )
  955. }
  956. guard let providerID = response.providerID, providerID.count > 0 else {
  957. throw AuthErrorUtils.unexpectedResponse(deserializedResponse: response)
  958. }
  959. let user = try await completeSignIn(withAccessToken: response.idToken,
  960. accessTokenExpirationDate: response
  961. .approximateExpirationDate,
  962. refreshToken: response.refreshToken,
  963. anonymous: false)
  964. let additionalUserInfo = AdditionalUserInfo(providerID: providerID,
  965. profile: response.profile,
  966. username: response.username,
  967. isNewUser: response.isNewUser)
  968. let updatedOAuthCredential = OAuthCredential(withVerifyAssertionResponse: response)
  969. return AuthDataResult(withUser: user,
  970. additionalUserInfo: additionalUserInfo,
  971. credential: updatedOAuthCredential)
  972. }
  973. #if os(iOS)
  974. /// Signs in using a phone credential.
  975. /// - Parameter credential: The Phone Auth credential used to sign in.
  976. /// - Parameter operation: The type of operation for which this sign-in attempt is initiated.
  977. private func signIn(withPhoneCredential credential: PhoneAuthCredential,
  978. operation: AuthOperationType) async throws -> VerifyPhoneNumberResponse {
  979. switch credential.credentialKind {
  980. case let .phoneNumber(phoneNumber, temporaryProof):
  981. let request = VerifyPhoneNumberRequest(temporaryProof: temporaryProof,
  982. phoneNumber: phoneNumber,
  983. operation: operation,
  984. requestConfiguration: requestConfiguration)
  985. return try await AuthBackend.call(with: request)
  986. case let .verification(verificationID, code):
  987. guard verificationID.count > 0 else {
  988. throw AuthErrorUtils.missingVerificationIDError(message: nil)
  989. }
  990. guard code.count > 0 else {
  991. throw AuthErrorUtils.missingVerificationCodeError(message: nil)
  992. }
  993. let request = VerifyPhoneNumberRequest(verificationID: verificationID,
  994. verificationCode: code,
  995. operation: operation,
  996. requestConfiguration: requestConfiguration)
  997. return try await AuthBackend.call(with: request)
  998. }
  999. }
  1000. #endif
  1001. #if !os(watchOS)
  1002. /// Signs in using a game center credential.
  1003. /// - Parameter credential: The Game Center Auth Credential used to sign in.
  1004. private func signInAndRetrieveData(withGameCenterCredential credential: GameCenterAuthCredential) async throws
  1005. -> AuthDataResult {
  1006. guard let publicKeyURL = credential.publicKeyURL,
  1007. let signature = credential.signature,
  1008. let salt = credential.salt else {
  1009. fatalError(
  1010. "Internal Auth Error: Game Center credential missing publicKeyURL, signature, or salt"
  1011. )
  1012. }
  1013. let request = SignInWithGameCenterRequest(playerID: credential.playerID,
  1014. teamPlayerID: credential.teamPlayerID,
  1015. gamePlayerID: credential.gamePlayerID,
  1016. publicKeyURL: publicKeyURL,
  1017. signature: signature,
  1018. salt: salt,
  1019. timestamp: credential.timestamp,
  1020. displayName: credential.displayName,
  1021. requestConfiguration: requestConfiguration)
  1022. let response = try await AuthBackend.call(with: request)
  1023. let user = try await completeSignIn(withAccessToken: response.idToken,
  1024. accessTokenExpirationDate: response
  1025. .approximateExpirationDate,
  1026. refreshToken: response.refreshToken,
  1027. anonymous: false)
  1028. let additionalUserInfo = AdditionalUserInfo(providerID: GameCenterAuthProvider.id,
  1029. profile: nil,
  1030. username: nil,
  1031. isNewUser: response.isNewUser)
  1032. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  1033. }
  1034. #endif
  1035. /// Signs in using an email and email sign-in link.
  1036. /// - Parameter email: The user's email address.
  1037. /// - Parameter link: The email sign-in link.
  1038. private func internalSignInAndRetrieveData(withEmail email: String,
  1039. link: String) async throws -> AuthDataResult {
  1040. guard let auth = requestConfiguration.auth, auth.isSignIn(withEmailLink: link) else {
  1041. fatalError("The link provided is not valid for email/link sign-in. Please check the link by " +
  1042. "calling isSignIn(withEmailLink:) on the Auth instance before attempting to use it " +
  1043. "for email/link sign-in.")
  1044. }
  1045. let queryItems = getQueryItems(link)
  1046. guard let actionCode = queryItems["oobCode"] else {
  1047. fatalError("Missing oobCode in link URL")
  1048. }
  1049. let request = EmailLinkSignInRequest(email: email,
  1050. oobCode: actionCode,
  1051. requestConfiguration: requestConfiguration)
  1052. let response = try await AuthBackend.call(with: request)
  1053. let user = try await completeSignIn(withAccessToken: response.idToken,
  1054. accessTokenExpirationDate: response
  1055. .approximateExpirationDate,
  1056. refreshToken: response.refreshToken,
  1057. anonymous: false)
  1058. let additionalUserInfo = AdditionalUserInfo(providerID: EmailAuthProvider.id,
  1059. profile: nil,
  1060. username: nil,
  1061. isNewUser: response.isNewUser)
  1062. return AuthDataResult(withUser: user, additionalUserInfo: additionalUserInfo)
  1063. }
  1064. private func getQueryItems(_ link: String) -> [String: String] {
  1065. var queryItems = AuthWebUtils.parseURL(link)
  1066. if queryItems.count == 0 {
  1067. let urlComponents = URLComponents(string: link)
  1068. if let query = urlComponents?.query {
  1069. queryItems = AuthWebUtils.parseURL(query)
  1070. }
  1071. }
  1072. return queryItems
  1073. }
  1074. private func internalSignInUser(withEmail email: String, password: String) async throws -> User {
  1075. let request = VerifyPasswordRequest(email: email,
  1076. password: password,
  1077. requestConfiguration: requestConfiguration)
  1078. if request.password.count == 0 {
  1079. throw AuthErrorUtils.wrongPasswordError(message: nil)
  1080. }
  1081. #if os(iOS)
  1082. let response = try await injectRecaptcha(request: request,
  1083. action: AuthRecaptchaAction.signInWithPassword)
  1084. #else
  1085. let response = try await AuthBackend.call(with: request)
  1086. #endif
  1087. return try await completeSignIn(
  1088. withAccessToken: response.idToken,
  1089. accessTokenExpirationDate: response.approximateExpirationDate,
  1090. refreshToken: response.refreshToken,
  1091. anonymous: false
  1092. )
  1093. }
  1094. #if os(iOS)
  1095. func injectRecaptcha<T: AuthRPCRequest>(request: T,
  1096. action: AuthRecaptchaAction) async throws -> T
  1097. .Response {
  1098. let recaptchaVerifier = AuthRecaptchaVerifier.shared(auth: requestConfiguration.auth)
  1099. if recaptchaVerifier.enablementStatus(forProvider: AuthRecaptchaProvider.password) {
  1100. try await recaptchaVerifier.injectRecaptchaFields(request: request,
  1101. provider: AuthRecaptchaProvider.password,
  1102. action: action)
  1103. } else {
  1104. do {
  1105. return try await AuthBackend.call(with: request)
  1106. } catch {
  1107. let nsError = error as NSError
  1108. if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? NSError,
  1109. nsError.code == AuthErrorCode.internalError.rawValue,
  1110. let messages = underlyingError
  1111. .userInfo[AuthErrorUtils.userInfoDeserializedResponseKey] as? [String: AnyHashable],
  1112. let message = messages["message"] as? String,
  1113. message.hasPrefix("MISSING_RECAPTCHA_TOKEN") {
  1114. try await recaptchaVerifier.injectRecaptchaFields(
  1115. request: request,
  1116. provider: AuthRecaptchaProvider.password,
  1117. action: action
  1118. )
  1119. } else {
  1120. throw error
  1121. }
  1122. }
  1123. }
  1124. return try await AuthBackend.call(with: request)
  1125. }
  1126. #endif
  1127. private func completeSignIn(withAccessToken accessToken: String?,
  1128. accessTokenExpirationDate: Date?,
  1129. refreshToken: String?,
  1130. anonymous: Bool) async throws -> User {
  1131. return try await User.retrieveUser(withAuth: requestConfiguration.auth!,
  1132. accessToken: accessToken,
  1133. accessTokenExpirationDate: accessTokenExpirationDate,
  1134. refreshToken: refreshToken,
  1135. anonymous: anonymous)
  1136. }
  1137. init(requestConfiguration: AuthRequestConfiguration) {
  1138. self.requestConfiguration = requestConfiguration
  1139. }
  1140. }