AuthViewController.swift 37 KB

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