AuthViewController.swift 36 KB

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