AuthViewController.swift 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373
  1. // Copyright 2020 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // For Sign in with Facebook
  15. import FBSDKLoginKit
  16. import FirebaseAuth
  17. // [START auth_import]
  18. import FirebaseCore
  19. // For Sign in with Google
  20. // [START google_import]
  21. import GoogleSignIn
  22. import UIKit
  23. // For Sign in with Apple
  24. import AuthenticationServices
  25. import CryptoKit
  26. private let kFacebookAppID = "ENTER APP ID HERE"
  27. class AuthViewController: UIViewController, DataSourceProviderDelegate {
  28. var dataSourceProvider: DataSourceProvider<AuthMenu>!
  29. override func loadView() {
  30. view = UITableView(frame: .zero, style: .insetGrouped)
  31. }
  32. override func viewDidLoad() {
  33. super.viewDidLoad()
  34. configureNavigationBar()
  35. configureDataSourceProvider()
  36. }
  37. // MARK: - DataSourceProviderDelegate
  38. func didSelectRowAt(_ indexPath: IndexPath, on tableView: UITableView) {
  39. let item = dataSourceProvider.item(at: indexPath)
  40. let providerName = item.isEditable ? item.detailTitle! : item.title!
  41. guard let provider = AuthMenu(rawValue: providerName) else {
  42. // The row tapped has no affiliated action.
  43. return
  44. }
  45. switch provider {
  46. case .settings:
  47. performSettings()
  48. case .google:
  49. performGoogleSignInFlow()
  50. case .apple:
  51. performAppleSignInFlow()
  52. case .facebook:
  53. performFacebookSignInFlow()
  54. case .twitter, .microsoft, .gitHub, .yahoo:
  55. performOAuthLoginFlow(for: provider)
  56. case .emailPassword:
  57. performDemoEmailPasswordLoginFlow()
  58. case .passwordless:
  59. performPasswordlessLoginFlow()
  60. case .phoneNumber:
  61. performPhoneNumberLoginFlow()
  62. case .anonymous:
  63. performAnonymousLoginFlow()
  64. case .custom:
  65. performCustomAuthLoginFlow()
  66. case .initRecaptcha:
  67. performInitRecaptcha()
  68. case .customAuthDomain:
  69. performCustomAuthDomainFlow()
  70. }
  71. }
  72. // MARK: - Firebase 🔥
  73. private func performSettings() {
  74. let settingsController = SettingsViewController()
  75. navigationController?.pushViewController(settingsController, animated: true)
  76. }
  77. private func performGoogleSignInFlow() {
  78. // [START headless_google_auth]
  79. guard let clientID = FirebaseApp.app()?.options.clientID else { return }
  80. // Create Google Sign In configuration object.
  81. // [START_EXCLUDE silent]
  82. // TODO: Move configuration to Info.plist
  83. // [END_EXCLUDE]
  84. let config = GIDConfiguration(clientID: clientID)
  85. GIDSignIn.sharedInstance.configuration = config
  86. // Start the sign in flow!
  87. GIDSignIn.sharedInstance.signIn(withPresenting: self) { [unowned self] result, error in
  88. guard error == nil else {
  89. // [START_EXCLUDE]
  90. return displayError(error)
  91. // [END_EXCLUDE]
  92. }
  93. guard let user = result?.user,
  94. let idToken = user.idToken?.tokenString
  95. else {
  96. // [START_EXCLUDE]
  97. let error = NSError(
  98. domain: "GIDSignInError",
  99. code: -1,
  100. userInfo: [
  101. NSLocalizedDescriptionKey: "Unexpected sign in result: required authentication data is missing.",
  102. ]
  103. )
  104. return displayError(error)
  105. // [END_EXCLUDE]
  106. }
  107. let credential = GoogleAuthProvider.credential(withIDToken: idToken,
  108. accessToken: user.accessToken.tokenString)
  109. // [START_EXCLUDE]
  110. signIn(with: credential)
  111. // [END_EXCLUDE]
  112. }
  113. // [END headless_google_auth]
  114. }
  115. func signIn(with credential: AuthCredential) {
  116. // [START signin_google_credential]
  117. AppManager.shared.auth().signIn(with: credential) { result, error in
  118. // [START_EXCLUDE silent]
  119. guard error == nil else { return self.displayError(error) }
  120. // [END_EXCLUDE]
  121. // At this point, our user is signed in
  122. // [START_EXCLUDE silent]
  123. // so we advance to the User View Controller
  124. self.transitionToUserViewController()
  125. // [END_EXCLUDE]
  126. }
  127. // [END signin_google_credential]
  128. }
  129. // For Sign in with Apple
  130. var currentNonce: String?
  131. private func performAppleSignInFlow() {
  132. do {
  133. let nonce = try CryptoUtils.randomNonceString()
  134. currentNonce = nonce
  135. let appleIDProvider = ASAuthorizationAppleIDProvider()
  136. let request = appleIDProvider.createRequest()
  137. request.requestedScopes = [.fullName, .email]
  138. request.nonce = CryptoUtils.sha256(nonce)
  139. let authorizationController = ASAuthorizationController(authorizationRequests: [request])
  140. authorizationController.delegate = self
  141. authorizationController.presentationContextProvider = self
  142. authorizationController.performRequests()
  143. } catch {
  144. // In the unlikely case that nonce generation fails, show error view.
  145. displayError(error)
  146. }
  147. }
  148. private func performFacebookSignInFlow() {
  149. // The following config can also be stored in the project's .plist
  150. Settings.shared.appID = kFacebookAppID
  151. Settings.shared.displayName = "AuthenticationExample"
  152. // Create a Facebook `LoginManager` instance
  153. let loginManager = LoginManager()
  154. loginManager.logIn(permissions: ["email"], from: self) { result, error in
  155. guard error == nil else { return self.displayError(error) }
  156. guard let accessToken = AccessToken.current else { return }
  157. let credential = FacebookAuthProvider.credential(withAccessToken: accessToken.tokenString)
  158. self.signin(with: credential)
  159. }
  160. }
  161. // Maintain a strong reference to an OAuthProvider for login
  162. private var oauthProvider: OAuthProvider!
  163. private func performOAuthLoginFlow(for provider: AuthMenu) {
  164. oauthProvider = OAuthProvider(providerID: provider.id)
  165. oauthProvider.getCredentialWith(nil) { credential, error in
  166. guard error == nil else { return self.displayError(error) }
  167. guard let credential = credential else { return }
  168. self.signin(with: credential)
  169. }
  170. }
  171. private func performDemoEmailPasswordLoginFlow() {
  172. let loginController = LoginController()
  173. loginController.delegate = self
  174. navigationController?.pushViewController(loginController, animated: true)
  175. }
  176. private func performPasswordlessLoginFlow() {
  177. let passwordlessViewController = PasswordlessViewController()
  178. passwordlessViewController.delegate = self
  179. let navPasswordlessAuthController =
  180. UINavigationController(rootViewController: passwordlessViewController)
  181. navigationController?.present(navPasswordlessAuthController, animated: true)
  182. }
  183. private func performPhoneNumberLoginFlow() {
  184. let phoneAuthViewController = PhoneAuthViewController()
  185. phoneAuthViewController.delegate = self
  186. let navPhoneAuthController = UINavigationController(rootViewController: phoneAuthViewController)
  187. navigationController?.present(navPhoneAuthController, animated: true)
  188. }
  189. private func performAnonymousLoginFlow() {
  190. AppManager.shared.auth().signInAnonymously { result, error in
  191. guard error == nil else { return self.displayError(error) }
  192. self.transitionToUserViewController()
  193. }
  194. }
  195. private func performCustomAuthLoginFlow() {
  196. let customAuthController = CustomAuthViewController()
  197. customAuthController.delegate = self
  198. let navCustomAuthController = UINavigationController(rootViewController: customAuthController)
  199. navigationController?.present(navCustomAuthController, animated: true)
  200. }
  201. private func signin(with credential: AuthCredential) {
  202. AppManager.shared.auth().signIn(with: credential) { result, error in
  203. guard error == nil else { return self.displayError(error) }
  204. self.transitionToUserViewController()
  205. }
  206. }
  207. private func performInitRecaptcha() {
  208. Task {
  209. do {
  210. try await AppManager.shared.auth().initializeRecaptchaConfig()
  211. print("Initializing Recaptcha config succeeded.")
  212. } catch {
  213. print("Initializing Recaptcha config failed: \(error).")
  214. }
  215. }
  216. }
  217. private func performCustomAuthDomainFlow() {
  218. let prompt = UIAlertController(title: nil, message: "Enter Custom Auth Domain For Auth:",
  219. preferredStyle: .alert)
  220. prompt.addTextField()
  221. let okAction = UIAlertAction(title: "OK", style: .default) { action in
  222. let domain = prompt.textFields?[0].text ?? ""
  223. AppManager.shared.auth().customAuthDomain = domain
  224. print("Successfully set auth domain to: \(domain)")
  225. }
  226. prompt.addAction(okAction)
  227. present(prompt, animated: true)
  228. }
  229. // MARK: - Private Helpers
  230. private func configureDataSourceProvider() {
  231. let tableView = view as! UITableView
  232. dataSourceProvider = DataSourceProvider(dataSource: AuthMenu.sections, tableView: tableView)
  233. dataSourceProvider.delegate = self
  234. }
  235. private func configureNavigationBar() {
  236. navigationItem.title = "Firebase Auth"
  237. guard let navigationBar = navigationController?.navigationBar else { return }
  238. navigationBar.prefersLargeTitles = true
  239. navigationBar.titleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  240. navigationBar.largeTitleTextAttributes = [.foregroundColor: UIColor.systemOrange]
  241. }
  242. private func transitionToUserViewController() {
  243. // UserViewController is at index 1 in the tabBarController.viewControllers array
  244. tabBarController?.transitionToViewController(atIndex: 1)
  245. }
  246. }
  247. // MARK: - LoginDelegate
  248. extension AuthViewController: LoginDelegate {
  249. public func loginDidOccur() {
  250. transitionToUserViewController()
  251. }
  252. }
  253. // MARK: - Implementing Sign in with Apple with Firebase
  254. extension AuthViewController: ASAuthorizationControllerDelegate,
  255. ASAuthorizationControllerPresentationContextProviding {
  256. // MARK: ASAuthorizationControllerDelegate
  257. func authorizationController(controller: ASAuthorizationController,
  258. didCompleteWithAuthorization authorization: ASAuthorization) {
  259. guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
  260. else {
  261. print("Unable to retrieve AppleIDCredential")
  262. return
  263. }
  264. guard let nonce = currentNonce else {
  265. fatalError("Invalid state: A login callback was received, but no login request was sent.")
  266. }
  267. guard let appleIDToken = appleIDCredential.identityToken else {
  268. print("Unable to fetch identity token")
  269. return
  270. }
  271. guard let appleAuthCode = appleIDCredential.authorizationCode else {
  272. print("Unable to fetch authorization code")
  273. return
  274. }
  275. guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
  276. print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
  277. return
  278. }
  279. guard let _ = String(data: appleAuthCode, encoding: .utf8) else {
  280. print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
  281. return
  282. }
  283. // use this call to create the authentication credential and set the user's full name
  284. let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
  285. rawNonce: nonce,
  286. fullName: appleIDCredential.fullName)
  287. AppManager.shared.auth().signIn(with: credential) { result, error in
  288. // Error. If error.code == .MissingOrInvalidNonce, make sure
  289. // you're sending the SHA256-hashed nonce as a hex string with
  290. // your request to Apple.
  291. guard error == nil else { return self.displayError(error) }
  292. // At this point, our user is signed in
  293. // so we advance to the User View Controller
  294. self.transitionToUserViewController()
  295. }
  296. }
  297. func authorizationController(controller: ASAuthorizationController,
  298. didCompleteWithError error: Error) {
  299. // Ensure that you have:
  300. // - enabled `Sign in with Apple` on the Firebase console
  301. // - added the `Sign in with Apple` capability for this project
  302. print("Sign in with Apple failed: \(error)")
  303. }
  304. // MARK: ASAuthorizationControllerPresentationContextProviding
  305. func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor {
  306. return view.window!
  307. }
  308. }