AuthViewController.swift 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088
  1. @testable import FirebaseAuth
  2. // Copyright 2020 Google LLC
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. // [START auth_import]
  16. import FirebaseCore
  17. import SwiftUI
  18. // For Sign in with Facebook
  19. import FBSDKLoginKit
  20. // For Sign in with Game Center
  21. import GameKit
  22. // For Sign in with Google
  23. // [START google_import]
  24. import GoogleSignIn
  25. import UIKit
  26. import SwiftUI
  27. // For Sign in with Apple
  28. import AuthenticationServices
  29. import CryptoKit
  30. private let kFacebookAppID = "ENTER APP ID HERE"
  31. private let kContinueUrl = "Enter URL"
  32. class AuthViewController: UIViewController, DataSourceProviderDelegate {
  33. var dataSourceProvider: DataSourceProvider<AuthMenuData>!
  34. var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
  35. var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
  36. var actionCodeContinueURL: URL?
  37. var actionCodeLinkDomain: String?
  38. var actionCodeRequestType: ActionCodeRequestType = .inApp
  39. let spinner = UIActivityIndicatorView(style: .medium)
  40. var tableView: UITableView { view as! UITableView }
  41. override func loadView() {
  42. view = UITableView(frame: .zero, style: .insetGrouped)
  43. }
  44. override func viewDidLoad() {
  45. super.viewDidLoad()
  46. configureNavigationBar()
  47. configureDataSourceProvider()
  48. }
  49. private func showSpinner() {
  50. spinner.center = view.center
  51. spinner.startAnimating()
  52. view.addSubview(spinner)
  53. }
  54. private func hideSpinner() {
  55. spinner.stopAnimating()
  56. spinner.removeFromSuperview()
  57. }
  58. private func actionCodeSettings() -> ActionCodeSettings {
  59. let settings = ActionCodeSettings()
  60. settings.url = actionCodeContinueURL
  61. settings.handleCodeInApp = (actionCodeRequestType == .inApp)
  62. settings.linkDomain = actionCodeLinkDomain
  63. return settings
  64. }
  65. // MARK: - DataSourceProviderDelegate
  66. func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
  67. let item = dataSourceProvider.item(at: indexPath)
  68. guard let providerName = item.title else {
  69. fatalError("Invalid item name")
  70. }
  71. guard let provider = AuthMenu(rawValue: providerName) else {
  72. // The row tapped has no affiliated action.
  73. return
  74. }
  75. switch provider {
  76. case .settings:
  77. performSettings()
  78. case .google:
  79. performGoogleSignInFlow()
  80. case .apple:
  81. performAppleSignInFlow()
  82. case .facebook:
  83. performFacebookSignInFlow()
  84. case .twitter, .microsoft, .gitHub, .yahoo, .linkedIn:
  85. performOAuthLoginFlow(for: provider)
  86. case .gameCenter:
  87. performGameCenterLoginFlow()
  88. case .emailPassword:
  89. performDemoEmailPasswordLoginFlow()
  90. case .passwordless:
  91. performPasswordlessLoginFlow()
  92. case .phoneNumber:
  93. performPhoneNumberLoginFlow()
  94. case .anonymous:
  95. performAnonymousLoginFlow()
  96. case .custom:
  97. performCustomAuthLoginFlow()
  98. case .initRecaptcha:
  99. performInitRecaptcha()
  100. case .customAuthDomain:
  101. performCustomAuthDomainFlow()
  102. case .getToken:
  103. getUserTokenResult(force: false)
  104. case .getTokenForceRefresh:
  105. getUserTokenResult(force: true)
  106. case .addAuthStateChangeListener:
  107. addAuthStateListener()
  108. case .removeLastAuthStateChangeListener:
  109. removeAuthStateListener()
  110. case .addIdTokenChangeListener:
  111. addIDTokenListener()
  112. case .removeLastIdTokenChangeListener:
  113. removeIDTokenListener()
  114. case .verifyClient:
  115. verifyClient()
  116. case .deleteApp:
  117. deleteApp()
  118. case .actionType:
  119. toggleActionCodeRequestType(at: indexPath)
  120. case .continueURL:
  121. changeActionCodeContinueURL(at: indexPath)
  122. case .linkDomain:
  123. changeActionCodeLinkDomain(at: indexPath)
  124. case .requestVerifyEmail:
  125. requestVerifyEmail()
  126. case .requestPasswordReset:
  127. requestPasswordReset()
  128. case .resetPassword:
  129. resetPassword()
  130. case .checkActionCode:
  131. checkActionCode()
  132. case .applyActionCode:
  133. applyActionCode()
  134. case .verifyPasswordResetCode:
  135. verifyPasswordResetCode()
  136. case .phoneEnroll:
  137. phoneEnroll()
  138. case .totpEnroll:
  139. Task { await totpEnroll() }
  140. case .multifactorUnenroll:
  141. mfaUnenroll()
  142. }
  143. }
  144. // MARK: - Firebase 🔥
  145. private func performSettings() {
  146. let settingsController = SettingsViewController()
  147. navigationController?.pushViewController(settingsController, animated: true)
  148. }
  149. private func performGoogleSignInFlow() {
  150. // [START headless_google_auth]
  151. guard let clientID = FirebaseApp.app()?.options.clientID else { return }
  152. // Create Google Sign In configuration object.
  153. // [START_EXCLUDE silent]
  154. // TODO: Move configuration to Info.plist
  155. // [END_EXCLUDE]
  156. let config = GIDConfiguration(clientID: clientID)
  157. GIDSignIn.sharedInstance.configuration = config
  158. Task {
  159. do {
  160. let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: self)
  161. let user = result.user
  162. guard let idToken = user.idToken?.tokenString
  163. else {
  164. // [START_EXCLUDE]
  165. let error = NSError(
  166. domain: "GIDSignInError",
  167. code: -1,
  168. userInfo: [
  169. NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
  170. ]
  171. )
  172. return displayError(error)
  173. // [END_EXCLUDE]
  174. }
  175. let credential = GoogleAuthProvider.credential(withIDToken: idToken,
  176. accessToken: user.accessToken.tokenString)
  177. try await signIn(with: credential)
  178. } catch {
  179. return displayError(error)
  180. }
  181. // [END_EXCLUDE]
  182. }
  183. // [END headless_google_auth]
  184. }
  185. func signIn(with credential: AuthCredential) async throws {
  186. do {
  187. _ = try await AppManager.shared.auth().signIn(with: credential)
  188. transitionToUserViewController()
  189. } catch {
  190. let authError = error as NSError
  191. if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
  192. let resolver = authError
  193. .userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
  194. performMfaLoginFlow(resolver: resolver)
  195. } else {
  196. return displayError(error)
  197. }
  198. }
  199. }
  200. // For Sign in with Apple
  201. var currentNonce: String?
  202. private func performAppleSignInFlow() {
  203. do {
  204. let nonce = try CryptoUtils.randomNonceString()
  205. currentNonce = nonce
  206. let appleIDProvider = ASAuthorizationAppleIDProvider()
  207. let request = appleIDProvider.createRequest()
  208. request.requestedScopes = [.fullName, .email]
  209. request.nonce = CryptoUtils.sha256(nonce)
  210. let authorizationController = ASAuthorizationController(authorizationRequests: [request])
  211. authorizationController.delegate = self
  212. authorizationController.presentationContextProvider = self
  213. authorizationController.performRequests()
  214. } catch {
  215. // In the unlikely case that nonce generation fails, show error view.
  216. displayError(error)
  217. }
  218. }
  219. private func performFacebookSignInFlow() {
  220. // The following config can also be stored in the project's .plist
  221. Settings.shared.appID = kFacebookAppID
  222. Settings.shared.displayName = "AuthenticationExample"
  223. // Create a Facebook `LoginManager` instance
  224. let loginManager = LoginManager()
  225. loginManager.logIn(permissions: ["email"], from: self) { result, error in
  226. guard error == nil else { return self.displayError(error) }
  227. guard let accessToken = AccessToken.current else { return }
  228. let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
  229. self.signin(with: credential)
  230. }
  231. }
  232. // Maintain a strong reference to an OAuthProvider for login
  233. private var oauthProvider: OAuthProvider!
  234. private func performOAuthLoginFlow(for provider: AuthMenu) {
  235. oauthProvider = OAuthProvider(providerID: provider.id)
  236. oauthProvider.getCredentialWith(nil) { credential, error in
  237. guard error == nil else { return self.displayError(error) }
  238. guard let credential = credential else { return }
  239. self.signin(with: credential)
  240. }
  241. }
  242. private func performGameCenterLoginFlow() {
  243. // Step 1: System Game Center Login
  244. GKLocalPlayer.local.authenticateHandler = { viewController, error in
  245. if let error = error {
  246. // Handle Game Center login error
  247. print("Error logging into Game Center: \(error.localizedDescription)")
  248. } else if let authViewController = viewController {
  249. // Present Game Center login UI if needed
  250. self.present(authViewController, animated: true)
  251. } else {
  252. // Game Center login successful, proceed to Firebase
  253. self.linkGameCenterToFirebase()
  254. }
  255. }
  256. }
  257. // Step 2: Link to Firebase
  258. private func linkGameCenterToFirebase() {
  259. GameCenterAuthProvider.getCredential { credential, error in
  260. if let error = error {
  261. // Handle Firebase credential retrieval error
  262. print("Error getting Game Center credential: \(error.localizedDescription)")
  263. } else if let credential = credential {
  264. Auth.auth().signIn(with: credential) { authResult, error in
  265. if let error = error {
  266. // Handle Firebase sign-in error
  267. print("Error signing into Firebase with Game Center: \(error.localizedDescription)")
  268. } else {
  269. // Firebase sign-in successful
  270. print("Successfully linked Game Center to Firebase")
  271. }
  272. }
  273. }
  274. }
  275. }
  276. private func performDemoEmailPasswordLoginFlow() {
  277. let loginView = LoginView(delegate: self)
  278. let hostingController = UIHostingController(rootView: loginView)
  279. hostingController.title = "Email/Password Auth"
  280. navigationController?.pushViewController(hostingController, animated: true)
  281. }
  282. private func performPasswordlessLoginFlow() {
  283. let passwordlessViewController = PasswordlessViewController()
  284. passwordlessViewController.delegate = self
  285. let navPasswordlessAuthController =
  286. UINavigationController(rootViewController: passwordlessViewController)
  287. navigationController?.present(navPasswordlessAuthController, animated: true)
  288. }
  289. private func performPhoneNumberLoginFlow() {
  290. let phoneAuthViewController = PhoneAuthViewController()
  291. phoneAuthViewController.delegate = self
  292. let navPhoneAuthController = UINavigationController(rootViewController: phoneAuthViewController)
  293. navigationController?.present(navPhoneAuthController, animated: true)
  294. }
  295. private func performMfaLoginFlow(resolver: MultiFactorResolver) {
  296. let mfaLoginController = UIHostingController(rootView: MFALoginView(
  297. resolver: resolver,
  298. delegate: self
  299. ))
  300. present(mfaLoginController, animated: true)
  301. }
  302. private func performAnonymousLoginFlow() {
  303. AppManager.shared.auth().signInAnonymously { result, error in
  304. guard error == nil else { return self.displayError(error) }
  305. self.transitionToUserViewController()
  306. }
  307. }
  308. private func performCustomAuthLoginFlow() {
  309. let customAuthController = CustomAuthViewController()
  310. customAuthController.delegate = self
  311. let navCustomAuthController = UINavigationController(rootViewController: customAuthController)
  312. navigationController?.present(navCustomAuthController, animated: true)
  313. }
  314. private func signin(with credential: AuthCredential) {
  315. AppManager.shared.auth().signIn(with: credential) { result, error in
  316. guard error == nil else { return self.displayError(error) }
  317. self.transitionToUserViewController()
  318. }
  319. }
  320. private func performInitRecaptcha() {
  321. Task {
  322. do {
  323. try await AppManager.shared.auth().initializeRecaptchaConfig()
  324. print("Initializing Recaptcha config succeeded.")
  325. } catch {
  326. print("Initializing Recaptcha config failed: \(error).")
  327. }
  328. }
  329. }
  330. private func performCustomAuthDomainFlow() {
  331. showTextInputPrompt(with: "Enter Custom Auth Domain For Auth: ", completion: { newDomain in
  332. AppManager.shared.auth().customAuthDomain = newDomain
  333. })
  334. }
  335. private func getUserTokenResult(force: Bool) {
  336. guard let currentUser = Auth.auth().currentUser else {
  337. print("Error: No user logged in")
  338. return
  339. }
  340. currentUser.getIDTokenResult(forcingRefresh: force, completion: { tokenResult, error in
  341. if error != nil {
  342. print("Error: Error refreshing token")
  343. return // Handle error case, returning early
  344. }
  345. if let tokenResult = tokenResult {
  346. let claims = tokenResult.claims
  347. var message = "Token refresh succeeded\n\n"
  348. for (key, value) in claims {
  349. message += "\(key): \(value)\n"
  350. }
  351. self.displayInfo(title: "Info", message: message, style: .alert)
  352. } else {
  353. print("Error: Unable to access claims.")
  354. }
  355. })
  356. }
  357. private func addAuthStateListener() {
  358. weak var weakSelf = self
  359. let index = authStateDidChangeListeners.count
  360. print("Auth State Did Change Listener #\(index) was added.")
  361. let handle = Auth.auth().addStateDidChangeListener { [weak weakSelf] auth, user in
  362. guard weakSelf != nil else { return }
  363. print("Auth State Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "nil")'")
  364. }
  365. authStateDidChangeListeners.append(handle)
  366. }
  367. private func removeAuthStateListener() {
  368. guard !authStateDidChangeListeners.isEmpty else {
  369. print("No remaining Auth State Did Change Listeners.")
  370. return
  371. }
  372. let index = authStateDidChangeListeners.count - 1
  373. let handle = authStateDidChangeListeners.last!
  374. Auth.auth().removeStateDidChangeListener(handle)
  375. authStateDidChangeListeners.removeLast()
  376. print("Auth State Did Change Listener #\(index) was removed.")
  377. }
  378. private func addIDTokenListener() {
  379. weak var weakSelf = self
  380. let index = IDTokenDidChangeListeners.count
  381. print("ID Token Did Change Listener #\(index) was added.")
  382. let handle = Auth.auth().addIDTokenDidChangeListener { [weak weakSelf] auth, user in
  383. guard weakSelf != nil else { return }
  384. print("ID Token Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "")'.")
  385. }
  386. IDTokenDidChangeListeners.append(handle)
  387. }
  388. private func removeIDTokenListener() {
  389. guard !IDTokenDidChangeListeners.isEmpty else {
  390. print("No remaining ID Token Did Change Listeners.")
  391. return
  392. }
  393. let index = IDTokenDidChangeListeners.count - 1
  394. let handle = IDTokenDidChangeListeners.last!
  395. Auth.auth().removeIDTokenDidChangeListener(handle)
  396. IDTokenDidChangeListeners.removeLast()
  397. print("ID Token Did Change Listener #\(index) was removed.")
  398. }
  399. private func verifyClient() {
  400. AppManager.shared.auth().tokenManager.getTokenInternal { result in
  401. guard case let .success(token) = result else {
  402. print("Verify iOS Client failed.")
  403. return
  404. }
  405. let request = VerifyClientRequest(
  406. withAppToken: token.string,
  407. isSandbox: token.type == .sandbox,
  408. requestConfiguration: AppManager.shared.auth().requestConfiguration
  409. )
  410. Task {
  411. do {
  412. let verifyResponse = try await AppManager.shared.auth().backend.call(with: request)
  413. guard let receipt = verifyResponse.receipt,
  414. let timeoutDate = verifyResponse.suggestedTimeOutDate else {
  415. print("Internal Auth Error: invalid VerifyClientResponse.")
  416. return
  417. }
  418. let timeout = timeoutDate.timeIntervalSinceNow
  419. do {
  420. let credential = await AppManager.shared.auth().appCredentialManager
  421. .didStartVerification(
  422. withReceipt: receipt,
  423. timeout: timeout
  424. )
  425. guard credential.secret != nil else {
  426. print("Failed to receive remote notification to verify App ID.")
  427. return
  428. }
  429. let testPhoneNumber = "+16509964692"
  430. let request = SendVerificationCodeRequest(
  431. phoneNumber: testPhoneNumber,
  432. codeIdentity: CodeIdentity.credential(credential),
  433. requestConfiguration: AppManager.shared.auth().requestConfiguration
  434. )
  435. do {
  436. _ = try await AppManager.shared.auth().backend.call(with: request)
  437. print("Verify iOS client succeeded")
  438. } catch {
  439. print("Verify iOS Client failed: \(error.localizedDescription)")
  440. }
  441. }
  442. } catch {
  443. print("Verify iOS Client failed: \(error.localizedDescription)")
  444. }
  445. }
  446. }
  447. }
  448. private func deleteApp() {
  449. AppManager.shared.app.delete { success in
  450. if success {
  451. print("App deleted successfully.")
  452. } else {
  453. print("Failed to delete app.")
  454. }
  455. }
  456. }
  457. private func toggleActionCodeRequestType(at indexPath: IndexPath) {
  458. switch actionCodeRequestType {
  459. case .inApp:
  460. actionCodeRequestType = .continue
  461. case .continue:
  462. actionCodeRequestType = .email
  463. case .email:
  464. actionCodeRequestType = .inApp
  465. }
  466. dataSourceProvider.updateItem(
  467. at: indexPath,
  468. item: Item(title: AuthMenu.actionType.name, detailTitle: actionCodeRequestType.name)
  469. )
  470. tableView.reloadData()
  471. }
  472. private func changeActionCodeContinueURL(at indexPath: IndexPath) {
  473. showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
  474. self.actionCodeContinueURL = URL(string: newContinueURL)
  475. print("Successfully set Continue URL to: \(newContinueURL)")
  476. self.dataSourceProvider.updateItem(
  477. at: indexPath,
  478. item: Item(
  479. title: AuthMenu.continueURL.name,
  480. detailTitle: self.actionCodeContinueURL?.absoluteString,
  481. isEditable: true
  482. )
  483. )
  484. self.tableView.reloadData()
  485. })
  486. }
  487. private func changeActionCodeLinkDomain(at indexPath: IndexPath) {
  488. showTextInputPrompt(with: "Link Domain:", completion: { newLinkDomain in
  489. self.actionCodeLinkDomain = newLinkDomain
  490. print("Successfully set Link Domain to: \(newLinkDomain)")
  491. self.dataSourceProvider.updateItem(
  492. at: indexPath,
  493. item: Item(
  494. title: AuthMenu.linkDomain.name,
  495. detailTitle: self.actionCodeLinkDomain,
  496. isEditable: true
  497. )
  498. )
  499. self.tableView.reloadData()
  500. })
  501. }
  502. private func requestVerifyEmail() {
  503. showSpinner()
  504. let completionHandler: ((any Error)?) -> Void = { [weak self] error in
  505. guard let self = self else { return }
  506. self.hideSpinner()
  507. if let error = error {
  508. let errorMessage = "Error sending verification email: \(error.localizedDescription)"
  509. showAlert(for: errorMessage)
  510. print(errorMessage)
  511. } else {
  512. let successMessage = "Verification email sent successfully!"
  513. showAlert(for: successMessage)
  514. print(successMessage)
  515. }
  516. }
  517. if actionCodeRequestType == .email {
  518. AppManager.shared.auth().currentUser?.sendEmailVerification(completion: completionHandler)
  519. } else {
  520. if actionCodeContinueURL == nil {
  521. print("Error: Action code continue URL is nil.")
  522. return
  523. }
  524. AppManager.shared.auth().currentUser?.sendEmailVerification(
  525. with: actionCodeSettings(),
  526. completion: completionHandler
  527. )
  528. }
  529. }
  530. func requestPasswordReset() {
  531. showTextInputPrompt(with: "Email:", completion: { email in
  532. print("Sending password reset link to: \(email)")
  533. self.showSpinner()
  534. let completionHandler: ((any Error)?) -> Void = { [weak self] error in
  535. guard let self = self else { return }
  536. self.hideSpinner()
  537. if let error = error {
  538. print("Request password reset failed: \(error)")
  539. showAlert(for: error.localizedDescription)
  540. return
  541. }
  542. print("Request password reset succeeded.")
  543. showAlert(for: "Sent!")
  544. }
  545. if self.actionCodeRequestType == .email {
  546. AppManager.shared.auth().sendPasswordReset(withEmail: email, completion: completionHandler)
  547. } else {
  548. guard self.actionCodeContinueURL != nil else {
  549. print("Error: Action code continue URL is nil.")
  550. return
  551. }
  552. AppManager.shared.auth().sendPasswordReset(
  553. withEmail: email,
  554. actionCodeSettings: self.actionCodeSettings(),
  555. completion: completionHandler
  556. )
  557. }
  558. })
  559. }
  560. private func resetPassword() {
  561. showSpinner()
  562. let completionHandler: ((any Error)?) -> Void = { [weak self] error in
  563. guard let self = self else { return }
  564. self.hideSpinner()
  565. if let error = error {
  566. print("Password reset failed \(error)")
  567. showAlert(for: error.localizedDescription)
  568. return
  569. }
  570. print("Password reset succeeded")
  571. showAlert(for: "Password reset succeeded!")
  572. }
  573. showTextInputPrompt(with: "OOB Code:") {
  574. code in
  575. self.showTextInputPrompt(with: "New Password") {
  576. password in
  577. AppManager.shared.auth().confirmPasswordReset(
  578. withCode: code,
  579. newPassword: password,
  580. completion: completionHandler
  581. )
  582. }
  583. }
  584. }
  585. private func nameForActionCodeOperation(_ operation: ActionCodeOperation) -> String {
  586. switch operation {
  587. case .verifyEmail:
  588. return "Verify Email"
  589. case .recoverEmail:
  590. return "Recover Email"
  591. case .passwordReset:
  592. return "Password Reset"
  593. case .emailLink:
  594. return "Email Sign-In Link"
  595. case .verifyAndChangeEmail:
  596. return "Verify Before Change Email"
  597. case .revertSecondFactorAddition:
  598. return "Revert Second Factor Addition"
  599. case .unknown:
  600. return "Unknown action"
  601. }
  602. }
  603. private func checkActionCode() {
  604. showSpinner()
  605. let completionHandler: (ActionCodeInfo?, (any Error)?) -> Void = { [weak self] info, error in
  606. guard let self = self else { return }
  607. self.hideSpinner()
  608. if let error = error {
  609. print("Check action code failed: \(error)")
  610. showAlert(for: error.localizedDescription)
  611. return
  612. }
  613. guard let info = info else { return }
  614. print("Check action code succeeded")
  615. let email = info.email
  616. let previousEmail = info.previousEmail
  617. let operation = self.nameForActionCodeOperation(info.operation)
  618. showAlert(for: operation, message: previousEmail ?? email)
  619. }
  620. showTextInputPrompt(with: "OOB Code:") {
  621. oobCode in
  622. AppManager.shared.auth().checkActionCode(oobCode, completion: completionHandler)
  623. }
  624. }
  625. private func applyActionCode() {
  626. showSpinner()
  627. let completionHandler: ((any Error)?) -> Void = { [weak self] error in
  628. guard let self = self else { return }
  629. self.hideSpinner()
  630. if let error = error {
  631. print("Apply action code failed \(error)")
  632. showAlert(for: error.localizedDescription)
  633. return
  634. }
  635. print("Apply action code succeeded")
  636. showAlert(for: "Action code was properly applied")
  637. }
  638. showTextInputPrompt(with: "OOB Code: ") {
  639. oobCode in
  640. AppManager.shared.auth().applyActionCode(oobCode, completion: completionHandler)
  641. }
  642. }
  643. private func verifyPasswordResetCode() {
  644. showSpinner()
  645. let completionHandler: (String?, (any Error)?) -> Void = { [weak self] email, error in
  646. guard let self = self else { return }
  647. self.hideSpinner()
  648. if let error = error {
  649. print("Verify password reset code failed \(error)")
  650. showAlert(for: error.localizedDescription)
  651. return
  652. }
  653. print("Verify password resest code succeeded.")
  654. showAlert(for: "Code verified for email: \(email ?? "missing email")")
  655. }
  656. showTextInputPrompt(with: "OOB Code: ") {
  657. oobCode in
  658. AppManager.shared.auth().verifyPasswordResetCode(oobCode, completion: completionHandler)
  659. }
  660. }
  661. private func phoneEnroll() {
  662. guard let user = AppManager.shared.auth().currentUser else {
  663. showAlert(for: "No user logged in!")
  664. print("Error: User must be logged in first.")
  665. return
  666. }
  667. showTextInputPrompt(with: "Phone Number:") { phoneNumber in
  668. user.multiFactor.getSessionWithCompletion { session, error in
  669. guard let session = session else { return }
  670. guard error == nil else {
  671. self.showAlert(for: "Enrollment failed")
  672. print("Multi factor start enroll failed. Error: \(error!)")
  673. return
  674. }
  675. PhoneAuthProvider.provider()
  676. .verifyPhoneNumber(phoneNumber, multiFactorSession: session) { verificationID, error in
  677. guard error == nil else {
  678. self.showAlert(for: "Enrollment failed")
  679. print("Multi factor start enroll failed. Error: \(error!)")
  680. return
  681. }
  682. self.showTextInputPrompt(with: "Verification Code: ") { verificationCode in
  683. let credential = PhoneAuthProvider.provider().credential(
  684. withVerificationID: verificationID!,
  685. verificationCode: verificationCode
  686. )
  687. let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
  688. self.showTextInputPrompt(with: "Display Name:") { displayName in
  689. user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
  690. if let error = error {
  691. self.showAlert(for: "Enrollment failed")
  692. print("Multi factor finalize enroll failed. Error: \(error)")
  693. } else {
  694. self.showAlert(for: "Successfully enrolled: \(displayName)")
  695. print("Multi factor finalize enroll succeeded.")
  696. }
  697. }
  698. }
  699. }
  700. }
  701. }
  702. }
  703. }
  704. private func totpEnroll() async {
  705. guard
  706. let user = AppManager.shared.auth().currentUser,
  707. let accountName = user.email
  708. else {
  709. showAlert(for: "Enrollment failed: User must be logged and have email address.")
  710. return
  711. }
  712. guard let issuer = AppManager.shared.auth().app?.name else {
  713. showAlert(for: "Enrollment failed: Firebase app is missing name.")
  714. return
  715. }
  716. do {
  717. let session = try await user.multiFactor.session()
  718. let secret = try await TOTPMultiFactorGenerator.generateSecret(with: session)
  719. print("Secret: " + secret.sharedSecretKey())
  720. let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
  721. guard !url.isEmpty else {
  722. showAlert(for: "Enrollment failed")
  723. print("Multi factor finalize enroll failed. Could not generate URL.")
  724. return
  725. }
  726. secret.openInOTPApp(withQRCodeURL: url)
  727. guard
  728. let oneTimePassword = await showTextInputPrompt(with: "Enter the one time passcode.")
  729. else {
  730. showAlert(for: "Enrollment failed: one time passcode not entered.")
  731. return
  732. }
  733. let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
  734. with: secret,
  735. oneTimePassword: oneTimePassword
  736. )
  737. // TODO(nickcooke): Provide option to enter display name.
  738. try await user.multiFactor.enroll(with: assertion, displayName: "TOTP")
  739. showAlert(for: "Successfully enrolled: TOTP")
  740. print("Multi factor finalize enroll succeeded.")
  741. } catch {
  742. print(error)
  743. showAlert(for: "Enrollment failed: \(error.localizedDescription)")
  744. }
  745. }
  746. func mfaUnenroll() {
  747. var displayNames: [String] = []
  748. guard let currentUser = Auth.auth().currentUser else {
  749. print("Error: No current user")
  750. return
  751. }
  752. for factorInfo in currentUser.multiFactor.enrolledFactors {
  753. if let displayName = factorInfo.displayName {
  754. displayNames.append(displayName)
  755. }
  756. }
  757. let alertController = UIAlertController(
  758. title: "Select Multi Factor to Unenroll",
  759. message: nil,
  760. preferredStyle: .actionSheet
  761. )
  762. for displayName in displayNames {
  763. let action = UIAlertAction(title: displayName, style: .default) { _ in
  764. self.unenrollFactor(with: displayName)
  765. }
  766. alertController.addAction(action)
  767. }
  768. let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
  769. alertController.addAction(cancelAction)
  770. present(alertController, animated: true, completion: nil)
  771. }
  772. private func unenrollFactor(with displayName: String) {
  773. guard let currentUser = Auth.auth().currentUser else {
  774. showAlert(for: "User must be logged in")
  775. print("Error: No current user")
  776. return
  777. }
  778. var factorInfoToUnenroll: MultiFactorInfo?
  779. for factorInfo in currentUser.multiFactor.enrolledFactors {
  780. if factorInfo.displayName == displayName {
  781. factorInfoToUnenroll = factorInfo
  782. break
  783. }
  784. }
  785. if let factorInfo = factorInfoToUnenroll {
  786. currentUser.multiFactor.unenroll(withFactorUID: factorInfo.uid) { error in
  787. if let error = error {
  788. self.showAlert(for: "Failed to unenroll factor: \(displayName)")
  789. print("Multi factor unenroll failed. Error: \(error.localizedDescription)")
  790. } else {
  791. self.showAlert(for: "Successfully unenrolled: \(displayName)")
  792. print("Multi factor unenroll succeeded.")
  793. }
  794. }
  795. }
  796. }
  797. // MARK: - Private Helpers
  798. private func showTextInputPrompt(with message: String, completion: ((String) -> Void)? = nil) {
  799. let editController = UIAlertController(
  800. title: message,
  801. message: nil,
  802. preferredStyle: .alert
  803. )
  804. editController.addTextField()
  805. let saveHandler: (UIAlertAction) -> Void = { _ in
  806. let text = editController.textFields?.first?.text ?? ""
  807. if let completion {
  808. completion(text)
  809. }
  810. }
  811. let cancelHandler: (UIAlertAction) -> Void = { _ in
  812. if let completion {
  813. completion("")
  814. }
  815. }
  816. editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler))
  817. editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancelHandler))
  818. // Assuming `self` is a view controller
  819. present(editController, animated: true, completion: nil)
  820. }
  821. private func showTextInputPrompt(with message: String) async -> String? {
  822. await withCheckedContinuation { continuation in
  823. showTextInputPrompt(with: message) { inputText in
  824. continuation.resume(returning: inputText.isEmpty ? nil : inputText)
  825. }
  826. }
  827. }
  828. // Function to generate QR code from a string
  829. private func generateQRCode(from string: String) -> UIImage? {
  830. let data = string.data(using: String.Encoding.ascii)
  831. if let filter = CIFilter(name: "CIQRCodeGenerator") {
  832. filter.setValue(data, forKey: "inputMessage")
  833. let transform = CGAffineTransform(scaleX: 10, y: 10)
  834. if let output = filter.outputImage?.transformed(by: transform) {
  835. return UIImage(ciImage: output)
  836. }
  837. }
  838. return nil
  839. }
  840. func showAlert(for title: String, message: String? = nil) {
  841. let alertController = UIAlertController(
  842. title: message,
  843. message: nil,
  844. preferredStyle: .alert
  845. )
  846. alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default))
  847. }
  848. private func configureDataSourceProvider() {
  849. dataSourceProvider = DataSourceProvider(
  850. dataSource: AuthMenuData.sections,
  851. tableView: tableView
  852. )
  853. dataSourceProvider.delegate = self
  854. }
  855. private func configureNavigationBar() {
  856. navigationItem.title = "Firebase Auth"
  857. guard let navigationBar = navigationController?.navigationBar else { return }
  858. navigationBar.prefersLargeTitles = true
  859. navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  860. navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  861. }
  862. private func transitionToUserViewController() {
  863. // UserViewController is at index 1 in the tabBarController.viewControllers array
  864. tabBarController?.transitionToViewController(atIndex: 1)
  865. }
  866. }
  867. // MARK: - LoginDelegate
  868. extension AuthViewController: LoginDelegate {
  869. public func loginDidOccur(resolver: MultiFactorResolver?) {
  870. if let resolver {
  871. performMfaLoginFlow(resolver: resolver)
  872. } else {
  873. transitionToUserViewController()
  874. }
  875. }
  876. }
  877. // MARK: - Implementing Sign in with Apple with Firebase
  878. extension AuthViewController: ASAuthorizationControllerDelegate,
  879. ASAuthorizationControllerPresentationContextProviding {
  880. // MARK: ASAuthorizationControllerDelegate
  881. func authorizationController(controller: ASAuthorizationController,
  882. didCompleteWithAuthorization authorization: ASAuthorization) {
  883. guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
  884. else {
  885. print("Unable to retrieve AppleIDCredential")
  886. return
  887. }
  888. guard let nonce = currentNonce else {
  889. fatalError("Invalid state: A login callback was received, but no login request was sent.")
  890. }
  891. guard let appleIDToken = appleIDCredential.identityToken else {
  892. print("Unable to fetch identity token")
  893. return
  894. }
  895. guard let appleAuthCode = appleIDCredential.authorizationCode else {
  896. print("Unable to fetch authorization code")
  897. return
  898. }
  899. guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
  900. print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
  901. return
  902. }
  903. guard let _ = String(data: appleAuthCode, encoding: .utf8) else {
  904. print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
  905. return
  906. }
  907. // use this call to create the authentication credential and set the user's full name
  908. let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
  909. rawNonce: nonce,
  910. fullName: appleIDCredential.fullName)
  911. AppManager.shared.auth().signIn(with: credential) { result, error in
  912. // Error. If error.code == .MissingOrInvalidNonce, make sure
  913. // you're sending the SHA256-hashed nonce as a hex string with
  914. // your request to Apple.
  915. guard error == nil else { return self.displayError(error) }
  916. // At this point, our user is signed in
  917. // so we advance to the User View Controller
  918. self.transitionToUserViewController()
  919. }
  920. }
  921. func authorizationController(controller: ASAuthorizationController,
  922. didCompleteWithError error: any Error) {
  923. // Ensure that you have:
  924. // - enabled `Sign in with Apple` on the Firebase console
  925. // - added the `Sign in with Apple` capability for this project
  926. print("Sign in with Apple failed: \(error)")
  927. }
  928. // MARK: ASAuthorizationControllerPresentationContextProviding
  929. func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
  930. return view.window!
  931. }
  932. }