| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218 |
- @testable import FirebaseAuth
- // Copyright 2020 Google LLC
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- // [START auth_import]
- import FirebaseCore
- import SwiftUI
- // For Sign in with Facebook
- import FBSDKLoginKit
- // For Sign in with Game Center
- import GameKit
- // For Sign in with Google
- // [START google_import]
- import GoogleSignIn
- import UIKit
- import SwiftUI
- // For Sign in with Apple
- import AuthenticationServices
- import CryptoKit
- private let kFacebookAppID = "ENTER APP ID HERE"
- private let kContinueUrl = "Enter URL"
- class AuthViewController: UIViewController, DataSourceProviderDelegate {
- var dataSourceProvider: DataSourceProvider<AuthMenuData>!
- var authStateDidChangeListeners: [AuthStateDidChangeListenerHandle] = []
- var IDTokenDidChangeListeners: [IDTokenDidChangeListenerHandle] = []
- var actionCodeContinueURL: URL?
- var actionCodeLinkDomain: String?
- var actionCodeRequestType: ActionCodeRequestType = .inApp
- let spinner = UIActivityIndicatorView(style: .medium)
- var tableView: UITableView { view as! UITableView }
- override func loadView() {
- view = UITableView(frame: .zero, style: .insetGrouped)
- }
- override func viewDidLoad() {
- super.viewDidLoad()
- configureNavigationBar()
- configureDataSourceProvider()
- }
- private func showSpinner() {
- spinner.center = view.center
- spinner.startAnimating()
- view.addSubview(spinner)
- }
- private func hideSpinner() {
- spinner.stopAnimating()
- spinner.removeFromSuperview()
- }
- private func actionCodeSettings() -> ActionCodeSettings {
- let settings = ActionCodeSettings()
- settings.url = actionCodeContinueURL
- settings.handleCodeInApp = (actionCodeRequestType == .inApp)
- settings.linkDomain = actionCodeLinkDomain
- return settings
- }
- // MARK: - DataSourceProviderDelegate
- func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
- let item = dataSourceProvider.item(at: indexPath)
- guard let providerName = item.title else {
- fatalError("Invalid item name")
- }
- guard let provider = AuthMenu(rawValue: providerName) else {
- // The row tapped has no affiliated action.
- return
- }
- switch provider {
- case .settings:
- performSettings()
- case .google:
- performGoogleSignInFlow()
- case .apple:
- performAppleSignInFlow()
- case .facebook:
- performFacebookSignInFlow()
- case .twitter, .microsoft, .gitHub, .yahoo, .linkedIn:
- performOAuthLoginFlow(for: provider)
- case .gameCenter:
- performGameCenterLoginFlow()
- case .emailPassword:
- performDemoEmailPasswordLoginFlow()
- case .passwordless:
- performPasswordlessLoginFlow()
- case .phoneNumber:
- performPhoneNumberLoginFlow()
- case .anonymous:
- performAnonymousLoginFlow()
- case .custom:
- performCustomAuthLoginFlow()
- case .initRecaptcha:
- performInitRecaptcha()
- case .customAuthDomain:
- performCustomAuthDomainFlow()
- case .getToken:
- getUserTokenResult(force: false)
- case .getTokenForceRefresh:
- getUserTokenResult(force: true)
- case .addAuthStateChangeListener:
- addAuthStateListener()
- case .removeLastAuthStateChangeListener:
- removeAuthStateListener()
- case .addIdTokenChangeListener:
- addIDTokenListener()
- case .removeLastIdTokenChangeListener:
- removeIDTokenListener()
- case .verifyClient:
- verifyClient()
- case .deleteApp:
- deleteApp()
- case .actionType:
- toggleActionCodeRequestType(at: indexPath)
- case .continueURL:
- changeActionCodeContinueURL(at: indexPath)
- case .linkDomain:
- changeActionCodeLinkDomain(at: indexPath)
- case .requestVerifyEmail:
- requestVerifyEmail()
- case .requestPasswordReset:
- requestPasswordReset()
- case .resetPassword:
- resetPassword()
- case .checkActionCode:
- checkActionCode()
- case .applyActionCode:
- applyActionCode()
- case .verifyPasswordResetCode:
- verifyPasswordResetCode()
- case .phoneEnroll:
- phoneEnroll()
- case .totpEnroll:
- Task { await totpEnroll() }
- case .multifactorUnenroll:
- mfaUnenroll()
- case .passkeySignUp:
- passkeySignUp()
- case .passkeyEnroll:
- Task { await passkeyEnroll() }
- case .passkeySignIn:
- Task { await passkeySignIn() }
- case .passkeyUnenroll:
- Task { await passkeyUnenroll() }
- }
- }
- // MARK: - Firebase 🔥
- private func performSettings() {
- let settingsController = SettingsViewController()
- navigationController?.pushViewController(settingsController, animated: true)
- }
- private func performGoogleSignInFlow() {
- // [START headless_google_auth]
- guard let clientID = FirebaseApp.app()?.options.clientID else { return }
- // Create Google Sign In configuration object.
- // [START_EXCLUDE silent]
- // TODO: Move configuration to Info.plist
- // [END_EXCLUDE]
- let config = GIDConfiguration(clientID: clientID)
- GIDSignIn.sharedInstance.configuration = config
- Task {
- do {
- let result = try await GIDSignIn.sharedInstance.signIn(withPresenting: self)
- let user = result.user
- guard let idToken = user.idToken?.tokenString
- else {
- // [START_EXCLUDE]
- let error = NSError(
- domain: "GIDSignInError",
- code: -1,
- userInfo: [
- NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
- ]
- )
- return displayError(error)
- // [END_EXCLUDE]
- }
- let credential = GoogleAuthProvider.credential(withIDToken: idToken,
- accessToken: user.accessToken.tokenString)
- try await signIn(with: credential)
- } catch {
- return displayError(error)
- }
- // [END_EXCLUDE]
- }
- // [END headless_google_auth]
- }
- func signIn(with credential: AuthCredential) async throws {
- do {
- _ = try await AppManager.shared.auth().signIn(with: credential)
- transitionToUserViewController()
- } catch {
- let authError = error as NSError
- if authError.code == AuthErrorCode.secondFactorRequired.rawValue {
- let resolver = authError
- .userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
- performMfaLoginFlow(resolver: resolver)
- } else {
- return displayError(error)
- }
- }
- }
- // For Sign in with Apple
- var currentNonce: String?
- private func performAppleSignInFlow() {
- do {
- let nonce = try CryptoUtils.randomNonceString()
- currentNonce = nonce
- let appleIDProvider = ASAuthorizationAppleIDProvider()
- let request = appleIDProvider.createRequest()
- request.requestedScopes = [.fullName, .email]
- request.nonce = CryptoUtils.sha256(nonce)
- let authorizationController = ASAuthorizationController(authorizationRequests: [request])
- authorizationController.delegate = self
- authorizationController.presentationContextProvider = self
- authorizationController.performRequests()
- } catch {
- // In the unlikely case that nonce generation fails, show error view.
- displayError(error)
- }
- }
- private func performFacebookSignInFlow() {
- // The following config can also be stored in the project's .plist
- Settings.shared.appID = kFacebookAppID
- Settings.shared.displayName = "AuthenticationExample"
- // Create a Facebook `LoginManager` instance
- let loginManager = LoginManager()
- loginManager.logIn(permissions: ["email"], from: self) { result, error in
- guard error == nil else { return self.displayError(error) }
- guard let accessToken = AccessToken.current else { return }
- let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
- self.signin(with: credential)
- }
- }
- // Maintain a strong reference to an OAuthProvider for login
- private var oauthProvider: OAuthProvider!
- private func performOAuthLoginFlow(for provider: AuthMenu) {
- oauthProvider = OAuthProvider(providerID: provider.id)
- oauthProvider.getCredentialWith(nil) { credential, error in
- guard error == nil else { return self.displayError(error) }
- guard let credential = credential else { return }
- self.signin(with: credential)
- }
- }
- private func performGameCenterLoginFlow() {
- // Step 1: System Game Center Login
- GKLocalPlayer.local.authenticateHandler = { viewController, error in
- if let error = error {
- // Handle Game Center login error
- print("Error logging into Game Center: \(error.localizedDescription)")
- } else if let authViewController = viewController {
- // Present Game Center login UI if needed
- self.present(authViewController, animated: true)
- } else {
- // Game Center login successful, proceed to Firebase
- self.linkGameCenterToFirebase()
- }
- }
- }
- // Step 2: Link to Firebase
- private func linkGameCenterToFirebase() {
- GameCenterAuthProvider.getCredential { credential, error in
- if let error = error {
- // Handle Firebase credential retrieval error
- print("Error getting Game Center credential: \(error.localizedDescription)")
- } else if let credential = credential {
- Auth.auth().signIn(with: credential) { authResult, error in
- if let error = error {
- // Handle Firebase sign-in error
- print("Error signing into Firebase with Game Center: \(error.localizedDescription)")
- } else {
- // Firebase sign-in successful
- print("Successfully linked Game Center to Firebase")
- }
- }
- }
- }
- }
- private func performDemoEmailPasswordLoginFlow() {
- let loginView = LoginView(delegate: self)
- let hostingController = UIHostingController(rootView: loginView)
- hostingController.title = "Email/Password Auth"
- navigationController?.pushViewController(hostingController, animated: true)
- }
- private func performPasswordlessLoginFlow() {
- let passwordlessViewController = PasswordlessViewController()
- passwordlessViewController.delegate = self
- let navPasswordlessAuthController =
- UINavigationController(rootViewController: passwordlessViewController)
- navigationController?.present(navPasswordlessAuthController, animated: true)
- }
- private func performPhoneNumberLoginFlow() {
- let phoneAuthViewController = PhoneAuthViewController()
- phoneAuthViewController.delegate = self
- let navPhoneAuthController = UINavigationController(rootViewController: phoneAuthViewController)
- navigationController?.present(navPhoneAuthController, animated: true)
- }
- private func performMfaLoginFlow(resolver: MultiFactorResolver) {
- let mfaLoginController = UIHostingController(rootView: MFALoginView(
- resolver: resolver,
- delegate: self
- ))
- present(mfaLoginController, animated: true)
- }
- private func performAnonymousLoginFlow() {
- AppManager.shared.auth().signInAnonymously { result, error in
- guard error == nil else { return self.displayError(error) }
- self.transitionToUserViewController()
- }
- }
- private func performCustomAuthLoginFlow() {
- let customAuthController = CustomAuthViewController()
- customAuthController.delegate = self
- let navCustomAuthController = UINavigationController(rootViewController: customAuthController)
- navigationController?.present(navCustomAuthController, animated: true)
- }
- private func signin(with credential: AuthCredential) {
- AppManager.shared.auth().signIn(with: credential) { result, error in
- guard error == nil else { return self.displayError(error) }
- self.transitionToUserViewController()
- }
- }
- private func performInitRecaptcha() {
- Task {
- do {
- try await AppManager.shared.auth().initializeRecaptchaConfig()
- print("Initializing Recaptcha config succeeded.")
- } catch {
- print("Initializing Recaptcha config failed: \(error).")
- }
- }
- }
- private func performCustomAuthDomainFlow() {
- showTextInputPrompt(with: "Enter Custom Auth Domain For Auth: ", completion: { newDomain in
- AppManager.shared.auth().customAuthDomain = newDomain
- })
- }
- private func getUserTokenResult(force: Bool) {
- guard let currentUser = Auth.auth().currentUser else {
- print("Error: No user logged in")
- return
- }
- currentUser.getIDTokenResult(forcingRefresh: force, completion: { tokenResult, error in
- if error != nil {
- print("Error: Error refreshing token")
- return // Handle error case, returning early
- }
- if let tokenResult = tokenResult {
- let claims = tokenResult.claims
- var message = "Token refresh succeeded\n\n"
- for (key, value) in claims {
- message += "\(key): \(value)\n"
- }
- self.displayInfo(title: "Info", message: message, style: .alert)
- } else {
- print("Error: Unable to access claims.")
- }
- })
- }
- private func addAuthStateListener() {
- weak var weakSelf = self
- let index = authStateDidChangeListeners.count
- print("Auth State Did Change Listener #\(index) was added.")
- let handle = Auth.auth().addStateDidChangeListener { [weak weakSelf] auth, user in
- guard weakSelf != nil else { return }
- print("Auth State Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "nil")'")
- }
- authStateDidChangeListeners.append(handle)
- }
- private func removeAuthStateListener() {
- guard !authStateDidChangeListeners.isEmpty else {
- print("No remaining Auth State Did Change Listeners.")
- return
- }
- let index = authStateDidChangeListeners.count - 1
- let handle = authStateDidChangeListeners.last!
- Auth.auth().removeStateDidChangeListener(handle)
- authStateDidChangeListeners.removeLast()
- print("Auth State Did Change Listener #\(index) was removed.")
- }
- private func addIDTokenListener() {
- weak var weakSelf = self
- let index = IDTokenDidChangeListeners.count
- print("ID Token Did Change Listener #\(index) was added.")
- let handle = Auth.auth().addIDTokenDidChangeListener { [weak weakSelf] auth, user in
- guard weakSelf != nil else { return }
- print("ID Token Did Change Listener #\(index) was invoked on user '\(user?.uid ?? "")'.")
- }
- IDTokenDidChangeListeners.append(handle)
- }
- private func removeIDTokenListener() {
- guard !IDTokenDidChangeListeners.isEmpty else {
- print("No remaining ID Token Did Change Listeners.")
- return
- }
- let index = IDTokenDidChangeListeners.count - 1
- let handle = IDTokenDidChangeListeners.last!
- Auth.auth().removeIDTokenDidChangeListener(handle)
- IDTokenDidChangeListeners.removeLast()
- print("ID Token Did Change Listener #\(index) was removed.")
- }
- private func verifyClient() {
- AppManager.shared.auth().tokenManager.getTokenInternal { result in
- guard case let .success(token) = result else {
- print("Verify iOS Client failed.")
- return
- }
- let request = VerifyClientRequest(
- withAppToken: token.string,
- isSandbox: token.type == .sandbox,
- requestConfiguration: AppManager.shared.auth().requestConfiguration
- )
- Task {
- do {
- let verifyResponse = try await AppManager.shared.auth().backend.call(with: request)
- guard let receipt = verifyResponse.receipt,
- let timeoutDate = verifyResponse.suggestedTimeOutDate else {
- print("Internal Auth Error: invalid VerifyClientResponse.")
- return
- }
- let timeout = timeoutDate.timeIntervalSinceNow
- do {
- let credential = await AppManager.shared.auth().appCredentialManager
- .didStartVerification(
- withReceipt: receipt,
- timeout: timeout
- )
- guard credential.secret != nil else {
- print("Failed to receive remote notification to verify App ID.")
- return
- }
- let testPhoneNumber = "+16509964692"
- let request = SendVerificationCodeRequest(
- phoneNumber: testPhoneNumber,
- codeIdentity: CodeIdentity.credential(credential),
- requestConfiguration: AppManager.shared.auth().requestConfiguration
- )
- do {
- _ = try await AppManager.shared.auth().backend.call(with: request)
- print("Verify iOS client succeeded")
- } catch {
- print("Verify iOS Client failed: \(error.localizedDescription)")
- }
- }
- } catch {
- print("Verify iOS Client failed: \(error.localizedDescription)")
- }
- }
- }
- }
- private func deleteApp() {
- AppManager.shared.app.delete { success in
- if success {
- print("App deleted successfully.")
- } else {
- print("Failed to delete app.")
- }
- }
- }
- private func toggleActionCodeRequestType(at indexPath: IndexPath) {
- switch actionCodeRequestType {
- case .inApp:
- actionCodeRequestType = .continue
- case .continue:
- actionCodeRequestType = .email
- case .email:
- actionCodeRequestType = .inApp
- }
- dataSourceProvider.updateItem(
- at: indexPath,
- item: Item(title: AuthMenu.actionType.name, detailTitle: actionCodeRequestType.name)
- )
- tableView.reloadData()
- }
- private func changeActionCodeContinueURL(at indexPath: IndexPath) {
- showTextInputPrompt(with: "Continue URL:", completion: { newContinueURL in
- self.actionCodeContinueURL = URL(string: newContinueURL)
- print("Successfully set Continue URL to: \(newContinueURL)")
- self.dataSourceProvider.updateItem(
- at: indexPath,
- item: Item(
- title: AuthMenu.continueURL.name,
- detailTitle: self.actionCodeContinueURL?.absoluteString,
- isEditable: true
- )
- )
- self.tableView.reloadData()
- })
- }
- private func changeActionCodeLinkDomain(at indexPath: IndexPath) {
- showTextInputPrompt(with: "Link Domain:", completion: { newLinkDomain in
- self.actionCodeLinkDomain = newLinkDomain
- print("Successfully set Link Domain to: \(newLinkDomain)")
- self.dataSourceProvider.updateItem(
- at: indexPath,
- item: Item(
- title: AuthMenu.linkDomain.name,
- detailTitle: self.actionCodeLinkDomain,
- isEditable: true
- )
- )
- self.tableView.reloadData()
- })
- }
- private func requestVerifyEmail() {
- showSpinner()
- let completionHandler: ((any Error)?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- let errorMessage = "Error sending verification email: \(error.localizedDescription)"
- showAlert(for: errorMessage)
- print(errorMessage)
- } else {
- let successMessage = "Verification email sent successfully!"
- showAlert(for: successMessage)
- print(successMessage)
- }
- }
- if actionCodeRequestType == .email {
- AppManager.shared.auth().currentUser?.sendEmailVerification(completion: completionHandler)
- } else {
- if actionCodeContinueURL == nil {
- print("Error: Action code continue URL is nil.")
- return
- }
- AppManager.shared.auth().currentUser?.sendEmailVerification(
- with: actionCodeSettings(),
- completion: completionHandler
- )
- }
- }
- func requestPasswordReset() {
- showTextInputPrompt(with: "Email:", completion: { email in
- print("Sending password reset link to: \(email)")
- self.showSpinner()
- let completionHandler: ((any Error)?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Request password reset failed: \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Request password reset succeeded.")
- showAlert(for: "Sent!")
- }
- if self.actionCodeRequestType == .email {
- AppManager.shared.auth().sendPasswordReset(withEmail: email, completion: completionHandler)
- } else {
- guard self.actionCodeContinueURL != nil else {
- print("Error: Action code continue URL is nil.")
- return
- }
- AppManager.shared.auth().sendPasswordReset(
- withEmail: email,
- actionCodeSettings: self.actionCodeSettings(),
- completion: completionHandler
- )
- }
- })
- }
- private func resetPassword() {
- showSpinner()
- let completionHandler: ((any Error)?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Password reset failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Password reset succeeded")
- showAlert(for: "Password reset succeeded!")
- }
- showTextInputPrompt(with: "OOB Code:") {
- code in
- self.showTextInputPrompt(with: "New Password") {
- password in
- AppManager.shared.auth().confirmPasswordReset(
- withCode: code,
- newPassword: password,
- completion: completionHandler
- )
- }
- }
- }
- private func nameForActionCodeOperation(_ operation: ActionCodeOperation) -> String {
- switch operation {
- case .verifyEmail:
- return "Verify Email"
- case .recoverEmail:
- return "Recover Email"
- case .passwordReset:
- return "Password Reset"
- case .emailLink:
- return "Email Sign-In Link"
- case .verifyAndChangeEmail:
- return "Verify Before Change Email"
- case .revertSecondFactorAddition:
- return "Revert Second Factor Addition"
- case .unknown:
- return "Unknown action"
- }
- }
- private func checkActionCode() {
- showSpinner()
- let completionHandler: (ActionCodeInfo?, (any Error)?) -> Void = { [weak self] info, error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Check action code failed: \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- guard let info = info else { return }
- print("Check action code succeeded")
- let email = info.email
- let previousEmail = info.previousEmail
- let operation = self.nameForActionCodeOperation(info.operation)
- showAlert(for: operation, message: previousEmail ?? email)
- }
- showTextInputPrompt(with: "OOB Code:") {
- oobCode in
- AppManager.shared.auth().checkActionCode(oobCode, completion: completionHandler)
- }
- }
- private func applyActionCode() {
- showSpinner()
- let completionHandler: ((any Error)?) -> Void = { [weak self] error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Apply action code failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Apply action code succeeded")
- showAlert(for: "Action code was properly applied")
- }
- showTextInputPrompt(with: "OOB Code: ") {
- oobCode in
- AppManager.shared.auth().applyActionCode(oobCode, completion: completionHandler)
- }
- }
- private func verifyPasswordResetCode() {
- showSpinner()
- let completionHandler: (String?, (any Error)?) -> Void = { [weak self] email, error in
- guard let self = self else { return }
- self.hideSpinner()
- if let error = error {
- print("Verify password reset code failed \(error)")
- showAlert(for: error.localizedDescription)
- return
- }
- print("Verify password resest code succeeded.")
- showAlert(for: "Code verified for email: \(email ?? "missing email")")
- }
- showTextInputPrompt(with: "OOB Code: ") {
- oobCode in
- AppManager.shared.auth().verifyPasswordResetCode(oobCode, completion: completionHandler)
- }
- }
- private func phoneEnroll() {
- guard let user = AppManager.shared.auth().currentUser else {
- showAlert(for: "No user logged in!")
- print("Error: User must be logged in first.")
- return
- }
- showTextInputPrompt(with: "Phone Number:") { phoneNumber in
- user.multiFactor.getSessionWithCompletion { session, error in
- guard let session = session else { return }
- guard error == nil else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed. Error: \(error!)")
- return
- }
- PhoneAuthProvider.provider()
- .verifyPhoneNumber(phoneNumber, multiFactorSession: session) { verificationID, error in
- guard error == nil else {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor start enroll failed. Error: \(error!)")
- return
- }
- self.showTextInputPrompt(with: "Verification Code: ") { verificationCode in
- let credential = PhoneAuthProvider.provider().credential(
- withVerificationID: verificationID!,
- verificationCode: verificationCode
- )
- let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
- self.showTextInputPrompt(with: "Display Name:") { displayName in
- user.multiFactor.enroll(with: assertion, displayName: displayName) { error in
- if let error = error {
- self.showAlert(for: "Enrollment failed")
- print("Multi factor finalize enroll failed. Error: \(error)")
- } else {
- self.showAlert(for: "Successfully enrolled: \(displayName)")
- print("Multi factor finalize enroll succeeded.")
- }
- }
- }
- }
- }
- }
- }
- }
- private func totpEnroll() async {
- guard
- let user = AppManager.shared.auth().currentUser,
- let accountName = user.email
- else {
- showAlert(for: "Enrollment failed: User must be logged and have email address.")
- return
- }
- guard let issuer = AppManager.shared.auth().app?.name else {
- showAlert(for: "Enrollment failed: Firebase app is missing name.")
- return
- }
- do {
- let session = try await user.multiFactor.session()
- let secret = try await TOTPMultiFactorGenerator.generateSecret(with: session)
- print("Secret: " + secret.sharedSecretKey())
- let url = secret.generateQRCodeURL(withAccountName: accountName, issuer: issuer)
- guard !url.isEmpty else {
- showAlert(for: "Enrollment failed")
- print("Multi factor finalize enroll failed. Could not generate URL.")
- return
- }
- secret.openInOTPApp(withQRCodeURL: url)
- guard
- let oneTimePassword = await showTextInputPrompt(with: "Enter the one time passcode.")
- else {
- showAlert(for: "Enrollment failed: one time passcode not entered.")
- return
- }
- let assertion = TOTPMultiFactorGenerator.assertionForEnrollment(
- with: secret,
- oneTimePassword: oneTimePassword
- )
- // TODO(nickcooke): Provide option to enter display name.
- try await user.multiFactor.enroll(with: assertion, displayName: "TOTP")
- showAlert(for: "Successfully enrolled: TOTP")
- print("Multi factor finalize enroll succeeded.")
- } catch {
- print(error)
- showAlert(for: "Enrollment failed: \(error.localizedDescription)")
- }
- }
- func mfaUnenroll() {
- var displayNames: [String] = []
- guard let currentUser = Auth.auth().currentUser else {
- print("Error: No current user")
- return
- }
- for factorInfo in currentUser.multiFactor.enrolledFactors {
- if let displayName = factorInfo.displayName {
- displayNames.append(displayName)
- }
- }
- let alertController = UIAlertController(
- title: "Select Multi Factor to Unenroll",
- message: nil,
- preferredStyle: .actionSheet
- )
- for displayName in displayNames {
- let action = UIAlertAction(title: displayName, style: .default) { _ in
- self.unenrollFactor(with: displayName)
- }
- alertController.addAction(action)
- }
- let cancelAction = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
- alertController.addAction(cancelAction)
- present(alertController, animated: true, completion: nil)
- }
- private func unenrollFactor(with displayName: String) {
- guard let currentUser = Auth.auth().currentUser else {
- showAlert(for: "User must be logged in")
- print("Error: No current user")
- return
- }
- var factorInfoToUnenroll: MultiFactorInfo?
- for factorInfo in currentUser.multiFactor.enrolledFactors {
- if factorInfo.displayName == displayName {
- factorInfoToUnenroll = factorInfo
- break
- }
- }
- if let factorInfo = factorInfoToUnenroll {
- currentUser.multiFactor.unenroll(withFactorUID: factorInfo.uid) { error in
- if let error = error {
- self.showAlert(for: "Failed to unenroll factor: \(displayName)")
- print("Multi factor unenroll failed. Error: \(error.localizedDescription)")
- } else {
- self.showAlert(for: "Successfully unenrolled: \(displayName)")
- print("Multi factor unenroll succeeded.")
- }
- }
- }
- }
- // MARK: - Passkey
- private func passkeySignUp() {
- guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
- print("OS version is not supported for this action.")
- return
- }
- Task {
- do {
- _ = try await AppManager.shared.auth().signInAnonymously()
- print("sign-in anonymously succeeded.")
- if let uid = AppManager.shared.auth().currentUser?.uid {
- print("User ID: \(uid)")
- }
- // Continue to enroll a passkey.
- await passkeyEnroll()
- } catch {
- print("sign-in anonymously failed: \(error.localizedDescription)")
- self.showAlert(for: "Anonymous Sign-In Failed")
- }
- }
- }
- private func passkeyEnroll() async {
- guard let user = AppManager.shared.auth().currentUser else {
- showAlert(for: "Please sign in first.")
- return
- }
- let passkeyName = await showTextInputPrompt(with: "Passkey name")
- guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
- showAlert(for: "Not Supported", message: "This OS version does not support passkeys.")
- return
- }
- do {
- let request = try await user.startPasskeyEnrollment(withName: passkeyName)
- let controller = ASAuthorizationController(authorizationRequests: [request])
- controller.delegate = self
- controller.presentationContextProvider = self
- controller.performRequests()
- print("Started passkey enrollment (challenge created).")
- } catch {
- showAlert(for: "Passkey enrollment failed", message: error.localizedDescription)
- print("startPasskeyEnrollment failed: \(error.localizedDescription)")
- }
- }
- private func passkeySignIn() async {
- guard #available(iOS 16.0, macOS 12.0, tvOS 16.0, *) else {
- print("OS version is not supported for this action.")
- return
- }
- do {
- let request = try await AppManager.shared.auth().startPasskeySignIn()
- let controller = ASAuthorizationController(authorizationRequests: [request])
- controller.delegate = self
- controller.presentationContextProvider = self
- controller.performRequests()
- print("Started passkey sign in (challenge created).")
- } catch {
- print("Passkey sign-in failed with error: \(error)")
- }
- }
- private func passkeyUnenroll() async {
- guard let user = AppManager.shared.auth().currentUser else {
- showAlert(for: "Please sign in first.")
- return
- }
- guard let credentialId = await showTextInputPrompt(with: "Credential Id") else {
- print("Passkey unenrollment cancelled: no credential id entered.")
- return
- }
- do {
- let _ = try await user.unenrollPasskey(withCredentialID: credentialId)
- } catch {
- showAlert(for: "Passkey unenrollment failed", message: error.localizedDescription)
- print("unenrollPasskey failed: \(error.localizedDescription)")
- }
- }
- // MARK: - Private Helpers
- private func showTextInputPrompt(with message: String, completion: ((String) -> Void)? = nil) {
- let editController = UIAlertController(
- title: message,
- message: nil,
- preferredStyle: .alert
- )
- editController.addTextField()
- let saveHandler: (UIAlertAction) -> Void = { _ in
- let text = editController.textFields?.first?.text ?? ""
- if let completion {
- completion(text)
- }
- }
- let cancelHandler: (UIAlertAction) -> Void = { _ in
- if let completion {
- completion("")
- }
- }
- editController.addAction(UIAlertAction(title: "Save", style: .default, handler: saveHandler))
- editController.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: cancelHandler))
- // Assuming `self` is a view controller
- present(editController, animated: true, completion: nil)
- }
- private func showTextInputPrompt(with message: String) async -> String? {
- await withCheckedContinuation { continuation in
- showTextInputPrompt(with: message) { inputText in
- continuation.resume(returning: inputText.isEmpty ? nil : inputText)
- }
- }
- }
- // Function to generate QR code from a string
- private func generateQRCode(from string: String) -> UIImage? {
- let data = string.data(using: String.Encoding.ascii)
- if let filter = CIFilter(name: "CIQRCodeGenerator") {
- filter.setValue(data, forKey: "inputMessage")
- let transform = CGAffineTransform(scaleX: 10, y: 10)
- if let output = filter.outputImage?.transformed(by: transform) {
- return UIImage(ciImage: output)
- }
- }
- return nil
- }
- func showAlert(for title: String, message: String? = nil) {
- let alertController = UIAlertController(
- title: message,
- message: nil,
- preferredStyle: .alert
- )
- alertController.addAction(UIAlertAction(title: "OK", style: UIAlertAction.Style.default))
- }
- private func configureDataSourceProvider() {
- dataSourceProvider = DataSourceProvider(
- dataSource: AuthMenuData.sections,
- tableView: tableView
- )
- dataSourceProvider.delegate = self
- }
- private func configureNavigationBar() {
- navigationItem.title = "Firebase Auth"
- guard let navigationBar = navigationController?.navigationBar else { return }
- navigationBar.prefersLargeTitles = true
- navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
- }
- private func transitionToUserViewController() {
- // UserViewController is at index 1 in the tabBarController.viewControllers array
- tabBarController?.transitionToViewController(atIndex: 1)
- }
- }
- // MARK: - LoginDelegate
- extension AuthViewController: LoginDelegate {
- public func loginDidOccur(resolver: MultiFactorResolver?) {
- if let resolver {
- performMfaLoginFlow(resolver: resolver)
- } else {
- transitionToUserViewController()
- }
- }
- }
- // MARK: - Implementing Sign in with Apple with Firebase
- extension AuthViewController: ASAuthorizationControllerDelegate,
- ASAuthorizationControllerPresentationContextProviding {
- // MARK: ASAuthorizationControllerDelegate
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithAuthorization authorization: ASAuthorization) {
- if #available(iOS 16.0, macOS 12.0, tvOS 16.0, *),
- let regCred = authorization.credential
- as? ASAuthorizationPlatformPublicKeyCredentialRegistration {
- Task { @MainActor [weak self] in
- guard let self else { return }
- do {
- guard let user = AppManager.shared.auth().currentUser else {
- self.showAlert(for: "Finalize failed", message: "No signed-in user.")
- return
- }
- _ = try await user.finalizePasskeyEnrollment(withPlatformCredential: regCred)
- self.showAlert(for: "Passkey Enrollment", message: "Succeeded")
- print("Passkey Enrollment succeeded.")
- } catch {
- self.showAlert(for: "Passkey Enrollment failed", message: error.localizedDescription)
- print("Finalize enrollment failed: \(error.localizedDescription)")
- }
- }
- return
- }
- if let assertion = authorization
- .credential as? ASAuthorizationPlatformPublicKeyCredentialAssertion {
- Task { @MainActor [weak self] in
- guard let self else { return }
- do {
- let _ = try await AppManager.shared.auth()
- .finalizePasskeySignIn(withPlatformCredential: assertion)
- self.showAlert(for: "Passkey Sign-In", message: "Succeeded")
- print("Passkey sign-in succeeded.")
- self.transitionToUserViewController()
- } catch {
- self.showAlert(for: "Passkey Sign-In failed", message: error.localizedDescription)
- print("Finalize passkey sign-in failed: \(error.localizedDescription)")
- }
- }
- return
- }
- guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
- else {
- print("Unable to retrieve AppleIDCredential")
- return
- }
- guard let nonce = currentNonce else {
- fatalError("Invalid state: A login callback was received, but no login request was sent.")
- }
- guard let appleIDToken = appleIDCredential.identityToken else {
- print("Unable to fetch identity token")
- return
- }
- guard let appleAuthCode = appleIDCredential.authorizationCode else {
- print("Unable to fetch authorization code")
- return
- }
- guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
- print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
- return
- }
- guard let _ = String(data: appleAuthCode, encoding: .utf8) else {
- print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
- return
- }
- // use this call to create the authentication credential and set the user's full name
- let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
- rawNonce: nonce,
- fullName: appleIDCredential.fullName)
- AppManager.shared.auth().signIn(with: credential) { result, error in
- // Error. If error.code == .MissingOrInvalidNonce, make sure
- // you're sending the SHA256-hashed nonce as a hex string with
- // your request to Apple.
- guard error == nil else { return self.displayError(error) }
- // At this point, our user is signed in
- // so we advance to the User View Controller
- self.transitionToUserViewController()
- }
- }
- func authorizationController(controller: ASAuthorizationController,
- didCompleteWithError error: any Error) {
- print("Apple authorization failed: \(error)")
- // for Sign In with Apple, ensure that you have:
- // - enabled `Sign in with Apple` on the Firebase console
- // - added the `Sign in with Apple` capability for this project
- }
- // MARK: ASAuthorizationControllerPresentationContextProviding
- func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
- return view.window!
- }
- }
|