AuthViewController.swift 37 KB

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