AuthViewController.swift 37 KB

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