AuthViewController.swift 40 KB

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