AuthViewController.swift 34 KB

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