AuthViewController.swift 39 KB

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