瀏覽代碼

MFA release cherry-picks (#5087)

* MFA beta (#4823)

* Capitalize (#5063)

Co-authored-by: Chuan Ren <renkelvin@gmail.com>
Ryan Wilson 6 年之前
父節點
當前提交
60e5c04eb8
共有 100 個文件被更改,包括 3351 次插入212 次删除
  1. 103 0
      Example/Auth/ApiTests/PhoneMultiFactorTests.swift
  2. 55 49
      Example/Auth/AuthSample/AuthSample.xcodeproj/project.pbxproj
  3. 1 1
      Example/Auth/E2eTests/FIRAuthE2eTests.m
  4. 1 0
      Example/Auth/Sample/MainViewController+App.h
  5. 2 0
      Example/Auth/Sample/MainViewController+Auth.h
  6. 2 0
      Example/Auth/Sample/MainViewController+AutoTests.h
  7. 2 0
      Example/Auth/Sample/MainViewController+Custom.h
  8. 2 1
      Example/Auth/Sample/MainViewController+Email.h
  9. 50 4
      Example/Auth/Sample/MainViewController+Email.m
  10. 2 0
      Example/Auth/Sample/MainViewController+Facebook.h
  11. 1 0
      Example/Auth/Sample/MainViewController+GameCenter.h
  12. 2 0
      Example/Auth/Sample/MainViewController+Google.h
  13. 50 4
      Example/Auth/Sample/MainViewController+Google.m
  14. 2 0
      Example/Auth/Sample/MainViewController+Internal.h
  15. 31 0
      Example/Auth/Sample/MainViewController+MultiFactor.h
  16. 112 0
      Example/Auth/Sample/MainViewController+MultiFactor.m
  17. 2 0
      Example/Auth/Sample/MainViewController+OAuth.h
  18. 2 2
      Example/Auth/Sample/MainViewController+OAuth.m
  19. 2 0
      Example/Auth/Sample/MainViewController+OOB.h
  20. 7 3
      Example/Auth/Sample/MainViewController+OOB.m
  21. 30 2
      Example/Auth/Sample/MainViewController+User.m
  22. 103 28
      Example/Auth/Sample/MainViewController.m
  23. 16 4
      Example/Auth/Sample/UserInfoViewController.m
  24. 0 16
      Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m
  25. 1 1
      Example/Auth/Tests/FIRAuthTests.m
  26. 3 3
      Example/Auth/Tests/FIRUserTests.m
  27. 126 16
      Firebase/Auth/Source/Auth/FIRAuth.m
  28. 80 24
      Firebase/Auth/Source/Auth/FIRAuthTokenResult.m
  29. 6 6
      Firebase/Auth/Source/Auth/FIRAuthTokenResult_Internal.h
  30. 26 0
      Firebase/Auth/Source/Auth/FIRAuth_Internal.h
  31. 5 2
      Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthCredential.m
  32. 1 1
      Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthCredential_Internal.h
  33. 216 9
      Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthProvider.m
  34. 126 0
      Firebase/Auth/Source/Backend/FIRAuthBackend+MultiFactor.h
  35. 86 0
      Firebase/Auth/Source/Backend/FIRAuthBackend+MultiFactor.m
  36. 22 0
      Firebase/Auth/Source/Backend/FIRAuthBackend.h
  37. 134 7
      Firebase/Auth/Source/Backend/FIRAuthBackend.m
  38. 6 1
      Firebase/Auth/Source/Backend/FIRIdentityToolkitRequest.h
  39. 46 9
      Firebase/Auth/Source/Backend/FIRIdentityToolkitRequest.m
  40. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRCreateAuthURIRequest.m
  41. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRDeleteAccountRequest.m
  42. 1 1
      Firebase/Auth/Source/Backend/RPC/FIREmailLinkSignInRequest.h
  43. 1 1
      Firebase/Auth/Source/Backend/RPC/FIREmailLinkSignInRequest.m
  44. 3 2
      Firebase/Auth/Source/Backend/RPC/FIRGetAccountInfoResponse.h
  45. 9 0
      Firebase/Auth/Source/Backend/RPC/FIRGetAccountInfoResponse.m
  46. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRResetPasswordRequest.m
  47. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRSecureTokenRequest.m
  48. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRSendVerificationCodeRequest.m
  49. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRSetAccountInfoRequest.m
  50. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRSignInWithGameCenterRequest.m
  51. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRSignUpNewUserRequest.m
  52. 5 2
      Firebase/Auth/Source/Backend/RPC/FIRVerifyAssertionResponse.h
  53. 12 0
      Firebase/Auth/Source/Backend/RPC/FIRVerifyAssertionResponse.m
  54. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRVerifyClientRequest.m
  55. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordRequest.m
  56. 5 2
      Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordResponse.h
  57. 12 0
      Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordResponse.m
  58. 1 1
      Firebase/Auth/Source/Backend/RPC/FIRVerifyPhoneNumberRequest.m
  59. 41 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentRequest.h
  60. 60 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentRequest.m
  61. 30 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentResponse.h
  62. 30 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentResponse.m
  63. 38 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentRequest.h
  64. 57 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentRequest.m
  65. 28 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentResponse.h
  66. 34 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentResponse.m
  67. 38 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInRequest.h
  68. 55 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInRequest.m
  69. 30 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInResponse.h
  70. 28 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInResponse.m
  71. 41 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInRequest.h
  72. 60 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInRequest.m
  73. 28 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInResponse.h
  74. 32 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInResponse.m
  75. 34 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFARequest.h
  76. 52 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFARequest.m
  77. 29 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFAResponse.h
  78. 32 0
      Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFAResponse.m
  79. 30 0
      Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProto.h
  80. 33 0
      Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProtoMFAEnrollment.h
  81. 43 0
      Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProtoMFAEnrollment.m
  82. 31 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneRequestInfo.h
  83. 46 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneRequestInfo.m
  84. 27 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneResponseInfo.h
  85. 33 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneResponseInfo.m
  86. 37 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneRequestInfo.h
  87. 74 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneRequestInfo.m
  88. 27 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneResponseInfo.h
  89. 33 0
      Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneResponseInfo.m
  90. 38 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactor+Internal.h
  91. 176 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactor.m
  92. 34 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorAssertion+Internal.h
  93. 29 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorAssertion.m
  94. 26 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorConstants.m
  95. 37 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorInfo+Internal.h
  96. 74 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorInfo.m
  97. 35 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorResolver+Internal.h
  98. 95 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorResolver.m
  99. 40 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorSession+Internal.h
  100. 53 0
      Firebase/Auth/Source/MultiFactor/FIRMultiFactorSession.m

+ 103 - 0
Example/Auth/ApiTests/PhoneMultiFactorTests.swift

@@ -0,0 +1,103 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+import XCTest
+
+let kNoSecondFactorUserEmail = "iosapitests+no_second_factor@gmail.com"
+let kNoSecondFactorUserPassword = "aaaaaa"
+
+let kPhoneSecondFactorPhoneNumber = "+16509999999"
+let kPhoneSecondFactorVerificationCode = "123456"
+let kPhoneSecondFactorDisplayName = "phone1"
+
+let kOneSecondFactorUserEmail = "iosapitests+one_phone_second_factor@gmail.com"
+let kOneSecondFactorUserPassword = "aaaaaa"
+
+class PhoneMultiFactorTests: FIRAuthApiTestsBase {
+
+  func testEnrollUnenroll() {
+    let enrollExpectation = self.expectation(description: "Enroll phone multi factor finished.")
+    let unenrollExpectation = self.expectation(description: "Unenroll phone multi factor finished.")
+    Auth.auth().signIn(withEmail: kNoSecondFactorUserEmail, password: kNoSecondFactorUserPassword) { (result, error) in
+      XCTAssertNil(error, "User normal sign in failed. Error: \(error!.localizedDescription)")
+
+      // Enroll
+      guard let user = result?.user else {
+        XCTFail("No valid user after attempted sign-in.")
+      }
+      user.multiFactor.getSessionWithCompletion({ (session, error) in
+        XCTAssertNil(error, "Get multi factor session failed. Error: \(error!.localizedDescription)")
+        PhoneAuthProvider.provider().verifyPhoneNumber(
+          kPhoneSecondFactorPhoneNumber,
+          uiDelegate: nil,
+          multiFactorSession: session) { (verificationId, error) in
+            XCTAssertNil(error, "Verify phone number failed. Error: \(error!.localizedDescription)")
+            let credential = PhoneAuthProvider.provider().credential(
+              withVerificationID: verificationId!,
+              verificationCode: kPhoneSecondFactorVerificationCode)
+            let assertion = PhoneMultiFactorGenerator.assertion(with: credential);
+            user?.multiFactor.enroll(with: assertion, displayName: kPhoneSecondFactorDisplayName) { (error) in
+              XCTAssertNil(error, "Phone multi factor enroll failed. Error: \(error!.localizedDescription)")
+              XCTAssertEqual(Auth.auth().currentUser?.multiFactor.enrolledFactors.first?.displayName, kPhoneSecondFactorDisplayName)
+              enrollExpectation.fulfill()
+
+              // Unenroll
+              user = Auth.auth().currentUser
+              user?.multiFactor.unenroll(with: (user?.multiFactor.enrolledFactors.first)!, completion: { (error) in
+                XCTAssertNil(error, "Phone multi factor unenroll failed. Error: \(error!.localizedDescription)")
+                XCTAssertEqual(Auth.auth().currentUser?.multiFactor.enrolledFactors.count, 0)
+                unenrollExpectation.fulfill()
+              })
+            }
+        }
+      })
+    }
+
+    self.waitForExpectations(timeout: 30) { error in
+      XCTAssertNil(error, "Failed to wait for enroll and unenroll phone multi factor finished. Error: \(error!.localizedDescription)")
+    }
+  }
+
+  func testSignInWithSecondFactor() {
+    let signInExpectation = self.expectation(description: "Sign in with phone multi factor finished.")
+    Auth.auth().signIn(withEmail: kOneSecondFactorUserEmail, password: kOneSecondFactorUserPassword) { (result, error) in
+      // SignIn
+      guard let error = error, error.code == AuthErrorCode.secondFactorRequired.rawValue else {
+        XCTFail("User sign in returns wrong error. Error: \(error!.localizedDescription)")
+      }
+      let resolver = error.userInfo["FIRAuthErrorUserInfoMultiFactorResolverKey"] as! MultiFactorResolver
+      let hint = resolver.hints.first as! PhoneMultiFactorInfo
+      PhoneAuthProvider.provider().verifyPhoneNumber(
+        with: hint,
+        uiDelegate: nil,
+        multiFactorSession: resolver.session) { (verificationId, error) in
+          XCTAssertNil(error, "Failed to verify phone number. Error: \(error!.localizedDescription)")
+          let credential = PhoneAuthProvider.provider().credential(
+            withVerificationID: verificationId!,
+            verificationCode: kPhoneSecondFactorVerificationCode)
+          let assertion = PhoneMultiFactorGenerator.assertion(with: credential);
+          resolver.resolveSignIn(with: assertion) { (authResult, error) in
+            XCTAssertNil(error, "Failed to sign in with phone multi factor. Error: \(error!.localizedDescription)")
+            signInExpectation.fulfill()
+          }
+      }
+    }
+
+    self.waitForExpectations(timeout: 300) { error in
+      XCTAssertNil(error, "Failed to wait for enroll and unenroll phone multi factor finished. Error: \(error!.localizedDescription)")
+    }
+  }
+}

+ 55 - 49
Example/Auth/AuthSample/AuthSample.xcodeproj/project.pbxproj

@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		400283EA23EA254B0006A298 /* MainViewController+MultiFactor.m in Sources */ = {isa = PBXBuildFile; fileRef = 400283E923EA254A0006A298 /* MainViewController+MultiFactor.m */; };
 		DE800B4722A2F8AF00AC9A23 /* MainViewController+Custom.m in Sources */ = {isa = PBXBuildFile; fileRef = DE800B1122A2F8AF00AC9A23 /* MainViewController+Custom.m */; };
 		DE800B4822A2F8AF00AC9A23 /* SettingsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = DE800B1222A2F8AF00AC9A23 /* SettingsViewController.m */; };
 		DE800B4922A2F8AF00AC9A23 /* MainViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE800B1322A2F8AF00AC9A23 /* MainViewController.xib */; };
@@ -69,6 +70,8 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		400283E823EA254A0006A298 /* MainViewController+MultiFactor.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MainViewController+MultiFactor.h"; sourceTree = "<group>"; };
+		400283E923EA254A0006A298 /* MainViewController+MultiFactor.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "MainViewController+MultiFactor.m"; sourceTree = "<group>"; };
 		DE800AF422A2F87E00AC9A23 /* AuthSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AuthSample.app; sourceTree = BUILT_PRODUCTS_DIR; };
 		DE800B0E22A2F8AF00AC9A23 /* AppManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppManager.h; sourceTree = "<group>"; };
 		DE800B0F22A2F8AF00AC9A23 /* MainViewController+AutoTests.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "MainViewController+AutoTests.h"; sourceTree = "<group>"; };
@@ -210,68 +213,70 @@
 		DE800B0D22A2F8AF00AC9A23 /* Sample */ = {
 			isa = PBXGroup;
 			children = (
-				DE800B6C22A2FFFF00AC9A23 /* AuthCredentials.h */,
 				DE800B6922A2FF8700AC9A23 /* Application.plist */,
-				DE800B6A22A2FF8700AC9A23 /* Sample.entitlements */,
+				DE800B1F22A2F8AF00AC9A23 /* ApplicationDelegate.h */,
+				DE800B3C22A2F8AF00AC9A23 /* ApplicationDelegate.m */,
+				DE800B2722A2F8AF00AC9A23 /* ApplicationTemplate.plist */,
+				DE800B0E22A2F8AF00AC9A23 /* AppManager.h */,
+				DE800B2D22A2F8AF00AC9A23 /* AppManager.m */,
+				DE800B6C22A2FFFF00AC9A23 /* AuthCredentials.h */,
+				DE800B4622A2F8AF00AC9A23 /* AuthCredentialsTemplate.h */,
+				DE800B2622A2F8AF00AC9A23 /* AuthProviders.h */,
+				DE800B4522A2F8AF00AC9A23 /* AuthProviders.m */,
+				DE800B1D22A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.h */,
+				DE800B3D22A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.m */,
+				DE800B1E22A2F8AF00AC9A23 /* FacebookAuthProvider.h */,
+				DE800B3B22A2F8AF00AC9A23 /* FacebookAuthProvider.m */,
+				DE800B3322A2F8AF00AC9A23 /* GoogleAuthProvider.h */,
+				DE800B1422A2F8AF00AC9A23 /* GoogleAuthProvider.m */,
 				DE800B6522A2FF7300AC9A23 /* GoogleService-Info_multi.plist */,
 				DE800B6622A2FF7300AC9A23 /* GoogleService-Info.plist */,
-				DE800B0E22A2F8AF00AC9A23 /* AppManager.h */,
-				DE800B0F22A2F8AF00AC9A23 /* MainViewController+AutoTests.h */,
-				DE800B1022A2F8AF00AC9A23 /* MainViewController+OOB.h */,
-				DE800B1122A2F8AF00AC9A23 /* MainViewController+Custom.m */,
-				DE800B1222A2F8AF00AC9A23 /* SettingsViewController.m */,
+				DE800B3222A2F8AF00AC9A23 /* Images.xcassets */,
+				DE800B2E22A2F8AF00AC9A23 /* main.m */,
+				DE800B4422A2F8AF00AC9A23 /* MainViewController.h */,
+				DE800B2922A2F8AF00AC9A23 /* MainViewController.m */,
 				DE800B1322A2F8AF00AC9A23 /* MainViewController.xib */,
-				DE800B1422A2F8AF00AC9A23 /* GoogleAuthProvider.m */,
-				DE800B1522A2F8AF00AC9A23 /* UserInfoViewController.h */,
-				DE800B1622A2F8AF00AC9A23 /* MainViewController+OAuth.h */,
 				DE800B1722A2F8AF00AC9A23 /* MainViewController+App.h */,
-				DE800B1822A2F8AF00AC9A23 /* MainViewController+User.m */,
-				DE800B1922A2F8AF00AC9A23 /* MainViewController+GameCenter.m */,
-				DE800B1A22A2F8AF00AC9A23 /* MainViewController+Phone.m */,
-				DE800B1B22A2F8AF00AC9A23 /* UserTableViewCell.m */,
-				DE800B1C22A2F8AF00AC9A23 /* MainViewController+Facebook.m */,
-				DE800B1D22A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.h */,
-				DE800B1E22A2F8AF00AC9A23 /* FacebookAuthProvider.h */,
-				DE800B1F22A2F8AF00AC9A23 /* ApplicationDelegate.h */,
-				DE800B2022A2F8AF00AC9A23 /* UIViewController+Alerts.m */,
-				DE800B2122A2F8AF00AC9A23 /* MainViewController+Google.h */,
-				DE800B2222A2F8AF00AC9A23 /* MainViewController+Email.m */,
+				DE800B2F22A2F8AF00AC9A23 /* MainViewController+App.m */,
+				DE800B3822A2F8AF00AC9A23 /* MainViewController+Auth.h */,
 				DE800B2322A2F8AF00AC9A23 /* MainViewController+Auth.m */,
-				DE800B2422A2F8AF00AC9A23 /* StaticContentTableViewManager.h */,
-				DE800B2522A2F8AF00AC9A23 /* SampleTemplate.entitlements */,
-				DE800B2622A2F8AF00AC9A23 /* AuthProviders.h */,
-				DE800B2722A2F8AF00AC9A23 /* ApplicationTemplate.plist */,
-				DE800B2822A2F8AF00AC9A23 /* UserInfoViewController.xib */,
-				DE800B2922A2F8AF00AC9A23 /* MainViewController.m */,
-				DE800B2A22A2F8AF00AC9A23 /* MainViewController+Custom.h */,
+				DE800B0F22A2F8AF00AC9A23 /* MainViewController+AutoTests.h */,
 				DE800B2B22A2F8AF00AC9A23 /* MainViewController+AutoTests.m */,
-				DE800B2C22A2F8AF00AC9A23 /* MainViewController+OOB.m */,
-				DE800B2D22A2F8AF00AC9A23 /* AppManager.m */,
-				DE800B2E22A2F8AF00AC9A23 /* main.m */,
-				DE800B2F22A2F8AF00AC9A23 /* MainViewController+App.m */,
+				DE800B2A22A2F8AF00AC9A23 /* MainViewController+Custom.h */,
+				DE800B1122A2F8AF00AC9A23 /* MainViewController+Custom.m */,
+				DE800B3722A2F8AF00AC9A23 /* MainViewController+Email.h */,
+				DE800B2222A2F8AF00AC9A23 /* MainViewController+Email.m */,
+				DE800B3E22A2F8AF00AC9A23 /* MainViewController+Facebook.h */,
+				DE800B1C22A2F8AF00AC9A23 /* MainViewController+Facebook.m */,
+				DE800B4222A2F8AF00AC9A23 /* MainViewController+GameCenter.h */,
+				DE800B1922A2F8AF00AC9A23 /* MainViewController+GameCenter.m */,
+				DE800B2122A2F8AF00AC9A23 /* MainViewController+Google.h */,
+				DE800B3922A2F8AF00AC9A23 /* MainViewController+Google.m */,
 				DE800B3022A2F8AF00AC9A23 /* MainViewController+Internal.h */,
+				400283E823EA254A0006A298 /* MainViewController+MultiFactor.h */,
+				400283E923EA254A0006A298 /* MainViewController+MultiFactor.m */,
+				DE800B1622A2F8AF00AC9A23 /* MainViewController+OAuth.h */,
 				DE800B3122A2F8AF00AC9A23 /* MainViewController+OAuth.m */,
-				DE800B3222A2F8AF00AC9A23 /* Images.xcassets */,
-				DE800B3322A2F8AF00AC9A23 /* GoogleAuthProvider.h */,
-				DE800B3422A2F8AF00AC9A23 /* UserInfoViewController.m */,
+				DE800B1022A2F8AF00AC9A23 /* MainViewController+OOB.h */,
+				DE800B2C22A2F8AF00AC9A23 /* MainViewController+OOB.m */,
+				DE800B3F22A2F8AF00AC9A23 /* MainViewController+Phone.h */,
+				DE800B1A22A2F8AF00AC9A23 /* MainViewController+Phone.m */,
+				DE800B4322A2F8AF00AC9A23 /* MainViewController+User.h */,
+				DE800B1822A2F8AF00AC9A23 /* MainViewController+User.m */,
+				DE800B6A22A2FF8700AC9A23 /* Sample.entitlements */,
+				DE800B2522A2F8AF00AC9A23 /* SampleTemplate.entitlements */,
 				DE800B3522A2F8AF00AC9A23 /* SettingsViewController.h */,
+				DE800B1222A2F8AF00AC9A23 /* SettingsViewController.m */,
+				DE800B4122A2F8AF00AC9A23 /* SettingsViewController.xib */,
+				DE800B2422A2F8AF00AC9A23 /* StaticContentTableViewManager.h */,
 				DE800B3622A2F8AF00AC9A23 /* StaticContentTableViewManager.m */,
-				DE800B3722A2F8AF00AC9A23 /* MainViewController+Email.h */,
-				DE800B3822A2F8AF00AC9A23 /* MainViewController+Auth.h */,
-				DE800B3922A2F8AF00AC9A23 /* MainViewController+Google.m */,
 				DE800B3A22A2F8AF00AC9A23 /* UIViewController+Alerts.h */,
-				DE800B3B22A2F8AF00AC9A23 /* FacebookAuthProvider.m */,
-				DE800B3C22A2F8AF00AC9A23 /* ApplicationDelegate.m */,
-				DE800B3D22A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.m */,
-				DE800B3E22A2F8AF00AC9A23 /* MainViewController+Facebook.h */,
-				DE800B3F22A2F8AF00AC9A23 /* MainViewController+Phone.h */,
+				DE800B2022A2F8AF00AC9A23 /* UIViewController+Alerts.m */,
+				DE800B1522A2F8AF00AC9A23 /* UserInfoViewController.h */,
+				DE800B3422A2F8AF00AC9A23 /* UserInfoViewController.m */,
+				DE800B2822A2F8AF00AC9A23 /* UserInfoViewController.xib */,
 				DE800B4022A2F8AF00AC9A23 /* UserTableViewCell.h */,
-				DE800B4122A2F8AF00AC9A23 /* SettingsViewController.xib */,
-				DE800B4222A2F8AF00AC9A23 /* MainViewController+GameCenter.h */,
-				DE800B4322A2F8AF00AC9A23 /* MainViewController+User.h */,
-				DE800B4422A2F8AF00AC9A23 /* MainViewController.h */,
-				DE800B4522A2F8AF00AC9A23 /* AuthProviders.m */,
-				DE800B4622A2F8AF00AC9A23 /* AuthCredentialsTemplate.h */,
+				DE800B1B22A2F8AF00AC9A23 /* UserTableViewCell.m */,
 			);
 			name = Sample;
 			path = ../Sample;
@@ -449,6 +454,7 @@
 				DE800B4F22A2F8AF00AC9A23 /* MainViewController+Facebook.m in Sources */,
 				DE800B4D22A2F8AF00AC9A23 /* MainViewController+Phone.m in Sources */,
 				DE800B4722A2F8AF00AC9A23 /* MainViewController+Custom.m in Sources */,
+				400283EA23EA254B0006A298 /* MainViewController+MultiFactor.m in Sources */,
 				DE800B5A22A2F8AF00AC9A23 /* MainViewController+App.m in Sources */,
 				DE800B6222A2F8AF00AC9A23 /* CustomTokenDataEntryViewController.m in Sources */,
 				DE800B6122A2F8AF00AC9A23 /* ApplicationDelegate.m in Sources */,

+ 1 - 1
Example/Auth/E2eTests/FIRAuthE2eTests.m

@@ -34,7 +34,7 @@
       grey_allOf(grey_kindOfClass([UILabel class]), grey_accessibilityLabel(@"OK"), nil);
 
   [[EarlGrey selectElementWithMatcher:
-#warning TODO Add accessibilityIdentifiers for the elements.
+    // TODO: Add accessibilityIdentifiers for the elements.
                  grey_kindOfClass(NSClassFromString(@"_UIAlertControllerView"))]
       performAction:grey_typeText(email)];
 

+ 1 - 0
Example/Auth/Sample/MainViewController+App.h

@@ -17,6 +17,7 @@
 #import <Foundation/Foundation.h>
 
 #import "MainViewController.h"
+
 #import "StaticContentTableViewManager.h"
 
 NS_ASSUME_NONNULL_BEGIN

+ 2 - 0
Example/Auth/Sample/MainViewController+Auth.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "FIRAuth.h"

+ 2 - 0
Example/Auth/Sample/MainViewController+AutoTests.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "StaticContentTableViewManager.h"

+ 2 - 0
Example/Auth/Sample/MainViewController+Custom.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "StaticContentTableViewManager.h"

+ 2 - 1
Example/Auth/Sample/MainViewController+Email.h

@@ -16,9 +16,10 @@
 
 #import <Foundation/Foundation.h>
 
-#import "FIRAuthCredential.h"
 #import "MainViewController.h"
+
 #import "FIRAuth.h"
+#import "FIRAuthCredential.h"
 #import "StaticContentTableViewManager.h"
 
 NS_ASSUME_NONNULL_BEGIN

+ 50 - 4
Example/Auth/Sample/MainViewController+Email.m

@@ -17,10 +17,15 @@
 #import "MainViewController+Email.h"
 
 #import "AppManager.h"
+#import "FIRMultiFactorResolver+Internal.h"
+#import "FIRMultiFactorSession+Internal.h"
+#import "FIRPhoneMultiFactorInfo.h"
 #import "MainViewController+Internal.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
+extern NSString *const FIRAuthErrorUserInfoMultiFactorResolverKey;
+
 typedef void (^ShowEmailDialogCompletion)(FIRAuthCredential *credential);
 
 @implementation MainViewController (Email)
@@ -103,13 +108,13 @@ typedef void (^ShowEmailDialogCompletion)(FIRAuthCredential *credential);
 }
 
 - (void)signInEmailPassword {
-  [self showTextInputPromptWithMessage:@"Email Address:"
+  [self showTextInputPromptWithMessage:@"Email Address"
                           keyboardType:UIKeyboardTypeEmailAddress
                        completionBlock:^(BOOL userPressedOK, NSString *_Nullable email) {
     if (!userPressedOK || !email.length) {
       return;
     }
-    [self showTextInputPromptWithMessage:@"Password:"
+    [self showTextInputPromptWithMessage:@"Password"
                         completionBlock:^(BOOL userPressedOK, NSString *_Nullable password) {
         if (!userPressedOK) {
           return;
@@ -121,12 +126,53 @@ typedef void (^ShowEmailDialogCompletion)(FIRAuthCredential *credential);
                                                NSError *_Nullable error) {
             [self hideSpinner:^{
               if (error) {
-                [self logFailure:@"sign-in with Email/Password failed" error:error];
+                if (error.code == FIRAuthErrorCodeSecondFactorRequired) {
+                  FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
+                  NSMutableString *displayNameString = [NSMutableString string];
+                  for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+                    [displayNameString appendString:tmpFactorInfo.displayName];
+                    [displayNameString appendString:@" "];
+                  }
+                  [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Select factor to sign in\n%@", displayNameString]
+                                       completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) {
+                    FIRPhoneMultiFactorInfo* selectedHint;
+                    for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+                      if ([displayName isEqualToString:tmpFactorInfo.displayName]) {
+                        selectedHint = (FIRPhoneMultiFactorInfo *)tmpFactorInfo;
+                      }
+                    }
+                  [FIRPhoneAuthProvider.provider
+                     verifyPhoneNumberWithMultiFactorInfo:selectedHint
+                     UIDelegate:nil
+                     multiFactorSession:resolver.session
+                     completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
+                      if (error) {
+                        [self logFailure:@"Multi factor start sign in failed." error:error];
+                      } else {
+                        [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Verification code for %@", selectedHint.displayName]
+                                             completionBlock:^(BOOL userPressedOK, NSString *_Nullable verificationCode) {
+                         FIRPhoneAuthCredential *credential =
+                         [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+                                                                      verificationCode:verificationCode];
+                         FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
+                         [resolver resolveSignInWithAssertion:assertion completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
+                           if (error) {
+                             [self logFailure:@"Multi factor finalize sign in failed." error:error];
+                           } else {
+                             [self logSuccess:@"Multi factor finalize sign in succeeded."];
+                           }
+                         }];
+                       }];
+                      }
+                    }];
+                  }];
+                } else {
+                  [self logFailure:@"sign-in with Email/Password failed" error:error];
+                }
               } else {
                 [self logSuccess:@"sign-in with Email/Password succeeded."];
                 [self log:[NSString stringWithFormat:@"UID: %@",authResult.user.uid]];
               }
-              [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In Error" error:error];
             }];
           }];
         }];

+ 2 - 0
Example/Auth/Sample/MainViewController+Facebook.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "StaticContentTableViewManager.h"

+ 1 - 0
Example/Auth/Sample/MainViewController+GameCenter.h

@@ -14,6 +14,7 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
 #import <GameKit/GameKit.h>
 
 #import "MainViewController.h"

+ 2 - 0
Example/Auth/Sample/MainViewController+Google.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "StaticContentTableViewManager.h"

+ 50 - 4
Example/Auth/Sample/MainViewController+Google.m

@@ -16,13 +16,18 @@
 
 #import "MainViewController+Google.h"
 
-#import "AuthProviders.h"
 #import "AppManager.h"
+#import "AuthProviders.h"
+#import "FIRMultiFactorResolver+Internal.h"
+#import "FIRMultiFactorSession+Internal.h"
 #import "FIROAuthProvider.h"
+#import "FIRPhoneMultiFactorInfo.h"
 #import "MainViewController+Internal.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
+extern NSString *const FIRAuthErrorUserInfoMultiFactorResolverKey;
+
 @implementation MainViewController (Google)
 
 - (StaticContentTableViewSection *)googleAuthSection {
@@ -51,7 +56,49 @@ NS_ASSUME_NONNULL_BEGIN
      FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
                                               NSError *_Nullable error) {
        if (error) {
-         [self logFailure:@"sign-in with provider failed" error:error];
+         if (error.code == FIRAuthErrorCodeSecondFactorRequired) {
+           FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
+           NSMutableString *displayNameString = [NSMutableString string];
+           for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+             [displayNameString appendString:tmpFactorInfo.displayName];
+             [displayNameString appendString:@" "];
+           }
+           [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Select factor to sign in\n%@", displayNameString]
+                                completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) {
+                                  FIRPhoneMultiFactorInfo* selectedHint;
+                                  for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+                                    if ([displayName isEqualToString:tmpFactorInfo.displayName]) {
+                                      selectedHint = (FIRPhoneMultiFactorInfo *)tmpFactorInfo;
+                                    }
+                                  }
+                                  [FIRPhoneAuthProvider.provider
+                                   verifyPhoneNumberWithMultiFactorInfo:selectedHint
+                                   UIDelegate:nil
+                                   multiFactorSession:resolver.session
+                                   completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
+                                                   if (error) {
+                                                     [self logFailure:@"Multi factor start sign in failed." error:error];
+                                                   } else {
+                                                     [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Verification code for %@", selectedHint.displayName]
+                                                                          completionBlock:^(BOOL userPressedOK, NSString *_Nullable verificationCode) {
+                                                                            FIRPhoneAuthCredential *credential =
+                                                                            [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+                                                                                                                         verificationCode:verificationCode];
+                                                                            FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
+                                                                            [resolver resolveSignInWithAssertion:assertion completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
+                                                                              if (error) {
+                                                                                [self logFailure:@"Multi factor finalize sign in failed." error:error];
+                                                                              } else {
+                                                                                [self logSuccess:@"Multi factor finalize sign in succeeded."];
+                                                                              }
+                                                                            }];
+                                                                          }];
+                                                   }
+                                                 }];
+                                }];
+         } else {
+           [self logFailure:@"sign-in with provider failed" error:error];
+         }
        } else {
          [self logSuccess:@"sign-in with provider succeeded."];
        }
@@ -66,9 +113,8 @@ NS_ASSUME_NONNULL_BEGIN
                                  completion:nil];
          }
        }
-       [self showTypicalUIForUserUpdateResultsWithTitle:@"Sign-In" error:error];
      };
-       [auth signInWithCredential:credential completion:completion];
+     [auth signInWithCredential:credential completion:completion];
    }
  }];
 }

+ 2 - 0
Example/Auth/Sample/MainViewController+Internal.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "FirebaseAuth.h"

+ 31 - 0
Example/Auth/Sample/MainViewController+MultiFactor.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#import "MainViewController.h"
+
+#import "StaticContentTableViewManager.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface MainViewController (MultiFactor)
+
+- (StaticContentTableViewSection *)multiFactorSection;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 112 - 0
Example/Auth/Sample/MainViewController+MultiFactor.m

@@ -0,0 +1,112 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "MainViewController+MultiFactor.h"
+
+#import "FIRAuth_Internal.h"
+#import "FIRUser_Internal.h"
+#import "FIRMultiFactorInfo.h"
+#import "FIRPhoneAuthProvider.h"
+#import "MainViewController+Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation MainViewController (MultiFactor)
+
+- (StaticContentTableViewSection *)multiFactorSection {
+  __weak typeof(self) weakSelf = self;
+  return [StaticContentTableViewSection sectionWithTitle:@"Multi Factor" cells:@[
+    [StaticContentTableViewCell cellWithTitle:@"Phone Enroll"
+                                       action:^{ [weakSelf phoneEnroll]; }],
+    [StaticContentTableViewCell cellWithTitle:@"Phone Unenroll"
+                                       action:^{ [weakSelf phoneUnenroll]; }],
+  ]];
+}
+
+- (void)phoneEnroll {
+  FIRUser *user = FIRAuth.auth.currentUser;
+  if (!user) {
+    [self logFailure:@"Please sign in first." error:nil];
+    return;
+  }
+  [self showTextInputPromptWithMessage:@"Phone Number"
+                       completionBlock:^(BOOL userPressedOK, NSString *_Nullable phoneNumber) {
+    [user.multiFactor
+     getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable session, NSError *_Nullable error) {
+      [FIRPhoneAuthProvider.provider verifyPhoneNumber:phoneNumber
+                                            UIDelegate:nil
+                                    multiFactorSession:session
+                                            completion:^(NSString * _Nullable verificationID,
+                                                         NSError * _Nullable error) {
+        if (error) {
+          [self logFailure:@"Multi factor start enroll failed." error:error];
+        } else {
+          [self showTextInputPromptWithMessage:@"Verification code"
+                               completionBlock:^(BOOL userPressedOK,
+                                                 NSString *_Nullable verificationCode) {
+            FIRPhoneAuthCredential *credential =
+            [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+                                                         verificationCode:verificationCode];
+            FIRMultiFactorAssertion *assertion =
+            [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
+            [self showTextInputPromptWithMessage:@"Display name"
+                                 completionBlock:^(BOOL userPressedOK,
+                                                   NSString *_Nullable displayName) {
+              [user.multiFactor enrollWithAssertion:assertion
+                                        displayName:displayName
+                                         completion:^(NSError *_Nullable error) {
+                if (error) {
+                  [self logFailure:@"Multi factor finalize enroll failed." error:error];
+                } else {
+                  [self logSuccess:@"Multi factor finalize enroll succeeded."];
+                }
+              }];
+            }];
+          }];
+        }
+      }];
+    }];
+  }];
+}
+
+- (void)phoneUnenroll {
+  NSMutableString *displayNameString = [NSMutableString string];
+  for (FIRMultiFactorInfo *tmpFactorInfo in FIRAuth.auth.currentUser.multiFactor.enrolledFactors) {
+    [displayNameString appendString:tmpFactorInfo.displayName];
+    [displayNameString appendString:@" "];
+  }
+  [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Multifactor Unenroll\n%@", displayNameString]
+                       completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) {
+    FIRMultiFactorInfo *factorInfo;
+    for (FIRMultiFactorInfo *tmpFactorInfo in FIRAuth.auth.currentUser.multiFactor.enrolledFactors) {
+      if ([displayName isEqualToString:tmpFactorInfo.displayName]) {
+        factorInfo = tmpFactorInfo;
+      }
+    }
+    [FIRAuth.auth.currentUser.multiFactor unenrollWithInfo:factorInfo
+                                                completion:^(NSError * _Nullable error) {
+      if (error) {
+        [self logFailure:@"Multi factor unenroll failed." error:error];
+      } else {
+        [self logSuccess:@"Multi factor unenroll succeeded."];
+      }
+    }];
+  }];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 2 - 0
Example/Auth/Sample/MainViewController+OAuth.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "StaticContentTableViewManager.h"

+ 2 - 2
Example/Auth/Sample/MainViewController+OAuth.m

@@ -361,9 +361,9 @@ NS_ASSUME_NONNULL_BEGIN
 
 - (void)authorizationController:(ASAuthorizationController *)controller didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
   ASAuthorizationAppleIDCredential* appleIDCredential = authorization.credential;
-  NSString *idToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]];
+  NSString *IDToken = [NSString stringWithUTF8String:[appleIDCredential.identityToken bytes]];
   FIROAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com"
-                                                                      IDToken:idToken
+                                                                      IDToken:IDToken
                                                                      rawNonce:@"REPLACE_ME_WITH_YOUR_RAW_NONCE"
                                                                   accessToken:nil];
 

+ 2 - 0
Example/Auth/Sample/MainViewController+OOB.h

@@ -14,6 +14,8 @@
  * limitations under the License.
  */
 
+#import <Foundation/Foundation.h>
+
 #import "MainViewController.h"
 
 #import "FirebaseAuth.h"

+ 7 - 3
Example/Auth/Sample/MainViewController+OOB.m

@@ -72,6 +72,10 @@ NS_ASSUME_NONNULL_BEGIN
       return @"Password Reset";
     case FIRActionCodeOperationEmailLink:
       return @"Email Sign-In Link";
+    case FIRActionCodeOperationVerifyAndChangeEmail:
+      return @"Verify Before Change Email";
+    case FIRActionCodeOperationRevertSecondFactorAddition:
+      return @"Revert Second Factor Addition";
     case FIRActionCodeOperationUnknown:
       return @"Unknown action";
   }
@@ -201,10 +205,10 @@ NS_ASSUME_NONNULL_BEGIN
            return;
          }
          [self logSuccess:@"Check action code succeeded."];
-         NSString *email = [info dataForKey:FIRActionCodeEmailKey];
-         NSString *fromEmail = [info dataForKey:FIRActionCodeFromEmailKey];
+         NSString *email = info.email;
+         NSString *previousEmail = info.previousEmail;
          NSString *message =
-         fromEmail ? [NSString stringWithFormat:@"%@ -> %@", fromEmail, email] : email;
+             previousEmail ? [NSString stringWithFormat:@"%@ -> %@", previousEmail, email] : email;
          NSString *operation = [self nameForActionCodeOperation:info.operation];
          [self showMessagePromptWithTitle:operation
                                   message:message

+ 30 - 2
Example/Auth/Sample/MainViewController+User.m

@@ -42,7 +42,9 @@ NS_ASSUME_NONNULL_BEGIN
     [StaticContentTableViewCell cellWithTitle:@"Reload User"
                                       action:^{ [weakSelf reloadUser]; }],
     [StaticContentTableViewCell cellWithTitle:@"Delete User"
-                                      action:^{ [weakSelf deleteAccount]; }],
+                                       action:^{ [weakSelf deleteAccount]; }],
+    [StaticContentTableViewCell cellWithTitle:@"Verify before update email"
+                                       action:^{ [weakSelf verifyBeforeUpdateEmail]; }],
     ]];
 }
 
@@ -60,7 +62,10 @@ NS_ASSUME_NONNULL_BEGIN
          if (error) {
            [self logFailure:@"set display name failed" error:error];
          } else {
-           [self logSuccess:@"set display name succeeded."];
+           [FIRAuth.auth.currentUser getIDTokenResultWithCompletion:^(FIRAuthTokenResult *_Nullable tokenResult,
+                                                                      NSError *_Nullable error) {
+             [self logSuccess:@"set display name succeeded."];
+           }];
          }
          [self showTypicalUIForUserUpdateResultsWithTitle:@"Set Display Name" error:error];
        }];
@@ -181,6 +186,29 @@ NS_ASSUME_NONNULL_BEGIN
  }];
 }
 
+- (void)verifyBeforeUpdateEmail {
+  [self showTextInputPromptWithMessage:@"Email Address:"
+                       completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {
+  if (!userPressedOK || !userInput.length) {
+   return;
+  }
+  [self showSpinner:^{
+    [[self user] sendEmailVerificationBeforeUpdatingEmail:userInput
+                                       actionCodeSettings:[self actionCodeSettings]
+                                               completion:^(NSError *_Nullable error) {
+      if (error) {
+        [self logFailure:@"verify before update email failed." error:error];
+      } else {
+       [self logSuccess:@"verify before update email succeeded."];
+      }
+      [self hideSpinner:^{
+        [self showTypicalUIForUserUpdateResultsWithTitle:@"Update Email" error:error];
+      }];
+    }];
+   }];
+  }];
+}
+
 - (void)updatePassword {
   [self showTextInputPromptWithMessage:@"New Password:"
                        completionBlock:^(BOOL userPressedOK, NSString *_Nullable userInput) {

+ 103 - 28
Example/Auth/Sample/MainViewController.m

@@ -14,7 +14,16 @@
  * limitations under the License.
  */
 
-#import "MainViewController.h"
+#import <objc/runtime.h>
+
+#import <FirebaseCore/FIRApp.h>
+#import <FirebaseCore/FIRAppInternal.h>
+
+#import "AppManager.h"
+#import "AuthCredentials.h"
+#import "FacebookAuthProvider.h"
+#import "FirebaseAuth.h"
+#import "GoogleAuthProvider.h"
 #import "MainViewController+App.h"
 #import "MainViewController+Auth.h"
 #import "MainViewController+AutoTests.h"
@@ -24,32 +33,16 @@
 #import "MainViewController+GameCenter.h"
 #import "MainViewController+Google.h"
 #import "MainViewController+Internal.h"
+#import "MainViewController+MultiFactor.h"
 #import "MainViewController+OAuth.h"
 #import "MainViewController+OOB.h"
 #import "MainViewController+Phone.h"
 #import "MainViewController+User.h"
-
-#import <objc/runtime.h>
-
-#import <FirebaseCore/FIRApp.h>
-#import <FirebaseCore/FIRAppInternal.h>
-
-#import "AppManager.h"
-#import "AuthCredentials.h"
-#import "FIRAdditionalUserInfo.h"
-#import "FIROAuthProvider.h"
-#import "FIRPhoneAuthCredential.h"
-#import "FIRPhoneAuthProvider.h"
-#import "FIRAuthTokenResult.h"
-#import "FirebaseAuth.h"
-#import "FacebookAuthProvider.h"
-#import "GoogleAuthProvider.h"
 #import "SettingsViewController.h"
 #import "StaticContentTableViewManager.h"
 #import "UIViewController+Alerts.h"
 #import "UserInfoViewController.h"
 #import "UserTableViewCell.h"
-#import "FIRAuth_Internal.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -61,6 +54,8 @@ static NSString *const kSwitchToInMemoryUserTitle = @"Switch to in memory user";
 
 static NSString *const kNewOrExistingUserToggleTitle = @"New or Existing User Toggle";
 
+extern NSString *const FIRAuthErrorUserInfoMultiFactorResolverKey;
+
 typedef void (^FIRTokenCallback)(NSString *_Nullable token, NSError *_Nullable error);
 
 @implementation MainViewController {
@@ -236,6 +231,10 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
         ]],
       // Auth
       [weakSelf authSection],
+      // User
+      [weakSelf userSection],
+      // Multi Factor
+      [weakSelf multiFactorSection],
       // Email Auth
       [weakSelf emailAuthSection],
       // Phone Auth
@@ -250,8 +249,6 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
       [weakSelf customAuthSection],
       // Game Center Auth
       [weakSelf gameCenterAuthSection],
-      // User
-      [weakSelf userSection],
       // App
       [weakSelf appSection],
       // OOB
@@ -323,14 +320,55 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
       FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
                                                NSError *_Nullable error) {
         if (error) {
-          [self logFailure:@"reauthenticate operation failed" error:error];
+          if (error.code == FIRAuthErrorCodeSecondFactorRequired) {
+            FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
+            NSMutableString *displayNameString = [NSMutableString string];
+            for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+              [displayNameString appendString:tmpFactorInfo.displayName];
+              [displayNameString appendString:@" "];
+            }
+            [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Select factor to reauthenticate\n%@", displayNameString]
+                                 completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) {
+                                   FIRPhoneMultiFactorInfo* selectedHint;
+                                   for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+                                     if ([displayName isEqualToString:tmpFactorInfo.displayName]) {
+                                       selectedHint = (FIRPhoneMultiFactorInfo *)tmpFactorInfo;
+                                     }
+                                   }
+                                   [FIRPhoneAuthProvider.provider
+                                    verifyPhoneNumberWithMultiFactorInfo:selectedHint
+                                    UIDelegate:nil
+                                    multiFactorSession:resolver.session
+                                    completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
+                                                    if (error) {
+                                                      [self logFailure:@"Multi factor start sign in failed." error:error];
+                                                    } else {
+                                                      [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Verification code for %@", selectedHint.displayName]
+                                                                           completionBlock:^(BOOL userPressedOK, NSString *_Nullable verificationCode) {
+                                                                             FIRPhoneAuthCredential *credential =
+                                                                             [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+                                                                                                                          verificationCode:verificationCode];
+                                                                             FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
+                                                                             [resolver resolveSignInWithAssertion:assertion completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
+                                                                               if (error) {
+                                                                                 [self logFailure:@"Multi factor finalize sign in failed." error:error];
+                                                                               } else {
+                                                                                 [self logSuccess:@"Multi factor finalize sign in succeeded."];
+                                                                               }
+                                                                             }];
+                                                                           }];
+                                                    }
+                                                  }];
+                                 }];
+          } else {
+            [self logFailure:@"reauthenticate operation failed." error:error];
+          }
         } else {
           [self logSuccess:@"reauthenticate operation succeeded."];
         }
         if (authResult.additionalUserInfo) {
           [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
         }
-        [self showTypicalUIForUserUpdateResultsWithTitle:@"Reauthenticate" error:error];
       };
       [user reauthenticateWithCredential:credential completion:completion];
     }
@@ -383,18 +421,55 @@ static NSDictionary<NSString *, NSString *> *parseURL(NSString *urlString) {
       FIRAuthDataResultCallback completion = ^(FIRAuthDataResult *_Nullable authResult,
                                                NSError *_Nullable error) {
         if (error) {
-          [self logFailure:@"link auth provider failed" error:error];
+          if (error.code == FIRAuthErrorCodeSecondFactorRequired) {
+            FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
+            NSMutableString *displayNameString = [NSMutableString string];
+            for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+              [displayNameString appendString:tmpFactorInfo.displayName];
+              [displayNameString appendString:@" "];
+            }
+            [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Select factor to link\n%@", displayNameString]
+                                 completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) {
+                                   FIRPhoneMultiFactorInfo* selectedHint;
+                                   for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) {
+                                     if ([displayName isEqualToString:tmpFactorInfo.displayName]) {
+                                       selectedHint = (FIRPhoneMultiFactorInfo *)tmpFactorInfo;
+                                     }
+                                   }
+                                   [FIRPhoneAuthProvider.provider
+                                    verifyPhoneNumberWithMultiFactorInfo:selectedHint
+                                    UIDelegate:nil
+                                    multiFactorSession:resolver.session
+                                    completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
+              if (error) {
+                [self logFailure:@"Multi factor start sign in failed." error:error];
+              } else {
+                [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Verification code for %@", selectedHint.displayName]
+                                     completionBlock:^(BOOL userPressedOK, NSString *_Nullable verificationCode) {
+                 FIRPhoneAuthCredential *credential =
+                 [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID
+                                                              verificationCode:verificationCode];
+                 FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
+                 [resolver resolveSignInWithAssertion:assertion completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
+                   if (error) {
+                     [self logFailure:@"Multi factor finalize sign in failed." error:error];
+                   } else {
+                     [self logSuccess:@"Multi factor finalize sign in succeeded."];
+                   }
+                 }];
+               }];
+              }
+            }];
+           }];
+          } else {
+            [self logFailure:@"link auth provider failed" error:error];
+          }
         } else {
           [self logSuccess:@"link auth provider succeeded."];
         }
         if (authResult.additionalUserInfo) {
           [self logSuccess:[self stringWithAdditionalUserInfo:authResult.additionalUserInfo]];
         }
-        if (retrieveData) {
-          [self showUIForAuthDataResultWithResult:authResult error:error];
-        } else {
-          [self showTypicalUIForUserUpdateResultsWithTitle:@"Link Account" error:error];
-        }
       };
       [user linkWithCredential:credential completion:completion];
     }

+ 16 - 4
Example/Auth/Sample/UserInfoViewController.m

@@ -66,13 +66,14 @@ static NSString *stringFromDate(NSDate *date) {
   NSMutableArray<StaticContentTableViewSection *> *sections = [@[
     [StaticContentTableViewSection sectionWithTitle:@"User" cells:@[
       [StaticContentTableViewCell cellWithTitle:@"anonymous" value:stringWithBool(_user.anonymous)],
-      [StaticContentTableViewCell cellWithTitle:@"Creation date"
+      [StaticContentTableViewCell cellWithTitle:@"creation date"
                                           value:stringFromDate(_user.metadata.creationDate)],
-      [StaticContentTableViewCell cellWithTitle:@"Last sign in date"
+      [StaticContentTableViewCell cellWithTitle:@"last sign in date"
                                           value:stringFromDate(_user.metadata.lastSignInDate)],
-      [StaticContentTableViewCell cellWithTitle:@"emailVerified"
+      [StaticContentTableViewCell cellWithTitle:@"email verified"
                                           value:stringWithBool(_user.emailVerified)],
-      [StaticContentTableViewCell cellWithTitle:@"refreshToken" value:_user.refreshToken],
+      [StaticContentTableViewCell cellWithTitle:@"refresh token" value:_user.refreshToken],
+      [StaticContentTableViewCell cellWithTitle:@"multi factor" value:[self multiFactorString]],
     ]]
   ] mutableCopy];
   [sections addObject:[self sectionWithUserInfo:_user]];
@@ -96,4 +97,15 @@ static NSString *stringFromDate(NSDate *date) {
   [self dismissViewControllerAnimated:YES completion:nil];
 }
 
+- (NSString *)multiFactorString {
+  NSMutableString *string = [NSMutableString string];
+
+  for (FIRMultiFactorInfo *info in _user.multiFactor.enrolledFactors) {
+    [string appendString:info.displayName];
+    [string appendString:@" "];
+  }
+
+  return string;
+}
+
 @end

+ 0 - 16
Example/Auth/Tests/FIRAuthBackendRPCImplementationTests.m

@@ -130,22 +130,6 @@ static NSString *const kTestValue = @"TestValue";
 
 @end
 
-/** @extension FIRAuthBackend
-    @brief This class extension exposes the otherwise private @c implementation method. We use this
-        here to directly call the @c postWithRequest:response:callback: method of
-        @c FIRAuthBackendRPCImplementation in some of the tests.
- */
-@interface FIRAuthBackend ()
-
-/** @fn implementation
-    @brief Exposes the otherwise private @c implementation method. We use this here to directly call
-        the @c postWithRequest:response:callback: method of @c FIRAuthBackendRPCImplementation in
-        some of the tests.
- */
-+ (FIRAuthBackendRPCImplementation *)implementation;
-
-@end
-
 /** @class FIRFakeRequest
     @brief Allows us to fake a request with deterministic request bodies and encoding errors
         returned from the @c FIRAuthRPCRequest-specified @c unencodedHTTPRequestBodyWithError:

+ 1 - 1
Example/Auth/Tests/FIRAuthTests.m

@@ -875,7 +875,7 @@ fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
     XCTAssertTrue([NSThread isMainThread]);
     XCTAssertNil(error);
     XCTAssertEqual(info.operation, FIRActionCodeOperationVerifyEmail);
-    XCTAssert([fakeNewEmail isEqualToString:[info dataForKey:FIRActionCodeEmailKey]]);
+    XCTAssert([fakeNewEmail isEqualToString:info.email]);
     [expectation fulfill];
   }];
   [self waitForExpectationsWithTimeout:kExpectationTimeout handler:nil];

+ 3 - 3
Example/Auth/Tests/FIRUserTests.m

@@ -2230,7 +2230,7 @@ static const NSTimeInterval kExpectationTimeout = 2;
     @brief Helper for testing the flow of a successful @c
         getIDTokenResultForcingRefresh:completion: call.
  */
-- (void)getIDTokenResultForcingRefreshSuccessWithIDToken:(NSString *)idToken {
+- (void)getIDTokenResultForcingRefreshSuccessWithIDToken:(NSString *)IDToken {
   id mockGetAccountInfoResponseUser = OCMClassMock([FIRGetAccountInfoResponseUser class]);
   OCMStub([mockGetAccountInfoResponseUser localID]).andReturn(kLocalID);
   OCMStub([mockGetAccountInfoResponseUser email]).andReturn(kEmail);
@@ -2246,7 +2246,7 @@ static const NSTimeInterval kExpectationTimeout = 2;
 
       dispatch_async(FIRAuthGlobalWorkQueue(), ^() {
         id mockSecureTokenResponse = OCMClassMock([FIRSecureTokenResponse class]);
-        OCMStub([mockSecureTokenResponse accessToken]).andReturn(idToken);
+        OCMStub([mockSecureTokenResponse accessToken]).andReturn(IDToken);
         callback(mockSecureTokenResponse, nil);
       });
     });
@@ -2255,7 +2255,7 @@ static const NSTimeInterval kExpectationTimeout = 2;
                                            NSError *_Nullable error) {
       XCTAssertTrue([NSThread isMainThread]);
       XCTAssertNil(error);
-      XCTAssertEqualObjects(tokenResult.token, idToken);
+      XCTAssertEqualObjects(tokenResult.token, IDToken);
       XCTAssertTrue(tokenResult.issuedAtDate &&
           [tokenResult.issuedAtDate isKindOfClass:[NSDate class]]);
       XCTAssertTrue(tokenResult.authDate && [tokenResult.authDate isKindOfClass:[NSDate class]]);

+ 126 - 16
Firebase/Auth/Source/Auth/FIRAuth.m

@@ -152,6 +152,18 @@ static NSString *const kRecoverEmailRequestType = @"RECOVER_EMAIL";
 */
 static NSString *const kEmailLinkSignInRequestType = @"EMAIL_SIGNIN";
 
+/** @var kVerifyAndChangeEmailRequestType
+    @brief The action code type value for verifing and changing email in the check action code
+           response.
+ */
+static NSString *const kVerifyAndChangeEmailRequestType = @"VERIFY_AND_CHANGE_EMAIL";
+
+/** @var kRevertSecondFactorAdditionRequestType
+    @brief The action code type value for reverting second factor addition in the check action code
+           response.
+ */
+static NSString *const kRevertSecondFactorAdditionRequestType = @"REVERT_SECOND_FACTOR_ADDITION";
+
 /** @var kMissingPasswordReason
     @brief The reason why the @c FIRAuthErrorCodeWeakPassword error is thrown.
     @remarks This error message will be localized in the future.
@@ -168,25 +180,42 @@ static NSMutableDictionary *gKeychainServiceNameForAppName;
 
 #pragma mark - FIRActionCodeInfo
 
-@implementation FIRActionCodeInfo {
-  /** @var _email
-      @brief The email address to which the code was sent. The new email address in the case of
-          FIRActionCodeOperationRecoverEmail.
-   */
-  NSString *_email;
+@interface FIRActionCodeInfo ()
 
-  /** @var _fromEmail
-      @brief The current email address in the case of FIRActionCodeOperationRecoverEmail.
-   */
-  NSString *_fromEmail;
-}
+/**
+    @brief The operation being performed.
+ */
+@property(nonatomic, readwrite) FIRActionCodeOperation operation;
+
+/** @property email
+    @brief The email address to which the code was sent. The new email address in the case of
+        FIRActionCodeOperationRecoverEmail.
+ */
+@property(nonatomic, nullable, readwrite, copy) NSString *email;
+
+/** @property previousEmail
+    @brief The current email address in the case of FIRActionCodeOperationRecoverEmail.
+ */
+@property(nonatomic, nullable, readwrite, copy) NSString *previousEmail;
 
-- (NSString *)dataForKey:(FIRActionDataKey)key{
+#if TARGET_OS_IOS
+/** @property multiFactorInfo
+    @brief The MultiFactorInfo object of the second factor to be reverted in case of
+        FIRActionCodeMultiFactorInfoKey.
+ */
+@property(nonatomic, nullable, readwrite) FIRMultiFactorInfo *multiFactorInfo;
+#endif
+
+@end
+
+@implementation FIRActionCodeInfo
+
+- (NSString *)dataForKey:(FIRActionDataKey)key {
   switch (key) {
     case FIRActionCodeEmailKey:
-      return _email;
+      return self.email;
     case FIRActionCodeFromEmailKey:
-      return _fromEmail;
+      return self.previousEmail;
   }
 }
 
@@ -198,7 +227,7 @@ static NSMutableDictionary *gKeychainServiceNameForAppName;
     _operation = operation;
     if (newEmail) {
       _email = [newEmail copy];
-      _fromEmail = [email copy];
+      _previousEmail = [email copy];
     } else {
       _email = [email copy];
     }
@@ -224,11 +253,92 @@ static NSMutableDictionary *gKeychainServiceNameForAppName;
   if ([requestType isEqualToString:kEmailLinkSignInRequestType]) {
     return FIRActionCodeOperationEmailLink;
   }
+  if ([requestType isEqualToString:kVerifyAndChangeEmailRequestType]) {
+    return FIRActionCodeOperationVerifyAndChangeEmail;
+  }
+  if ([requestType isEqualToString:kRevertSecondFactorAdditionRequestType]) {
+    return FIRActionCodeOperationRevertSecondFactorAddition;
+  }
   return FIRActionCodeOperationUnknown;
 }
 
 @end
 
+#pragma mark - FIRActionCodeURL
+
+@implementation FIRActionCodeURL
+
+/** @fn FIRAuthParseURL:NSString
+    @brief Parses an incoming URL into all available query items.
+    @param urlString The url to be parsed.
+    @return A dictionary of available query items in the target URL.
+ */
++ (NSDictionary<NSString *, NSString *> *)parseURL:(NSString *)urlString {
+  NSString *linkURL = [NSURLComponents componentsWithString:urlString].query;
+  if (!linkURL) {
+    return @{};
+  }
+  NSArray<NSString *> *URLComponents = [linkURL componentsSeparatedByString:@"&"];
+  NSMutableDictionary<NSString *, NSString *> *queryItems =
+      [[NSMutableDictionary alloc] initWithCapacity:URLComponents.count];
+  for (NSString *component in URLComponents) {
+    NSRange equalRange = [component rangeOfString:@"="];
+    if (equalRange.location != NSNotFound) {
+      NSString *queryItemKey =
+          [[component substringToIndex:equalRange.location] stringByRemovingPercentEncoding];
+      NSString *queryItemValue =
+          [[component substringFromIndex:equalRange.location + 1] stringByRemovingPercentEncoding];
+      if (queryItemKey && queryItemValue) {
+        queryItems[queryItemKey] = queryItemValue;
+      }
+    }
+  }
+  return queryItems;
+}
+
++ (nullable instancetype)actionCodeURLWithLink:(NSString *)link {
+  NSDictionary<NSString *, NSString *> *queryItems = [FIRActionCodeURL parseURL:link];
+  if (!queryItems.count) {
+    NSURLComponents *urlComponents = [NSURLComponents componentsWithString:link];
+    queryItems = [FIRActionCodeURL parseURL:urlComponents.query];
+  }
+  if (!queryItems.count) {
+    return nil;
+  }
+  NSString *APIKey = queryItems[@"apiKey"];
+  NSString *actionCode = queryItems[@"oobCode"];
+  NSString *continueURLString = queryItems[@"continueUrl"];
+  NSString *languageCode = queryItems[@"languageCode"];
+  NSString *mode = queryItems[@"mode"];
+  NSString *tenantID = queryItems[@"tenantID"];
+  return [[FIRActionCodeURL alloc] initWithAPIKey:APIKey
+                                       actionCode:actionCode
+                                continueURLString:continueURLString
+                                     languageCode:languageCode
+                                             mode:mode
+                                         tenantID:tenantID];
+}
+
+- (nullable instancetype)initWithAPIKey:(NSString *)APIKey
+                             actionCode:(NSString *)actionCode
+                      continueURLString:(NSString *)continueURLString
+                           languageCode:(NSString *)languageCode
+                                   mode:(NSString *)mode
+                               tenantID:(NSString *)tenantID {
+
+  self = [super init];
+  if (self) {
+    _APIKey = APIKey;
+    _operation = [FIRActionCodeInfo actionCodeOperationForRequestType:mode];
+    _code = actionCode;
+    _continueURL = [NSURL URLWithString:continueURLString];
+    _languageCode = languageCode;
+  }
+  return self;
+}
+
+@end
+
 #pragma mark - FIRAuth
 
 #if TARGET_OS_IOS
@@ -1063,7 +1173,7 @@ static NSMutableDictionary *gKeychainServiceNameForAppName;
         completion(nil, error);
         return;
       }
-      completion([info dataForKey:FIRActionCodeEmailKey], nil);
+      completion(info.email, nil);
     }
   }];
 }

+ 80 - 24
Firebase/Auth/Source/Auth/FIRAuthTokenResult.m

@@ -16,6 +16,8 @@
 
 #import "FIRAuthTokenResult_Internal.h"
 
+#import "FIRAuthErrorUtils.h"
+
 NS_ASSUME_NONNULL_BEGIN
 
 /** @var kExpirationDateKey
@@ -43,6 +45,11 @@ static NSString *const kIssuedDateKey = @"issuedDate";
  */
 static NSString *const kSignInProviderKey = @"signInProvider";
 
+/** @var kSignInSecondFactorKey
+ @brief The key used to encode the signInSecondFactor property for NSSecureCoding.
+ */
+static NSString *const kSignInSecondFactorKey = @"signInSecondFactor";
+
 /** @var kClaimsKey
     @brief The key used to encode the claims property for NSSecureCoding.
  */
@@ -55,6 +62,7 @@ static NSString *const kClaimsKey = @"claims";
                      authDate:(NSDate *)authDate
                  issuedAtDate:(NSDate *)issuedAtDate
                signInProvider:(NSString *)signInProvider
+           signInSecondFactor:(NSString *)signInSecondFactor
                        claims:(NSDictionary *)claims {
   self = [super init];
   if (self) {
@@ -63,11 +71,81 @@ static NSString *const kClaimsKey = @"claims";
     _authDate = authDate;
     _issuedAtDate = issuedAtDate;
     _signInProvider = signInProvider;
+    _signInSecondFactor = signInSecondFactor;
     _claims = claims;
   }
   return self;
 }
 
++ (nullable FIRAuthTokenResult *)tokenResultWithToken:(NSString *)token {
+  NSArray *tokenStringArray = [token componentsSeparatedByString:@"."];
+
+  // The JWT should have three parts, though we only use the second in this method.
+  if (tokenStringArray.count != 3) {
+    return nil;
+  }
+
+  // The token payload is always the second index of the array.
+  NSString *IDToken = tokenStringArray[1];
+
+  // Convert the base64URL encoded string to a base64 encoded string.
+  // Replace "_" with "/"
+  NSMutableString *tokenPayload =
+  [[IDToken stringByReplacingOccurrencesOfString:@"_" withString:@"/"] mutableCopy];
+
+  // Replace "-" with "+"
+  [tokenPayload replaceOccurrencesOfString:@"-"
+                                withString:@"+"
+                                   options:kNilOptions
+                                     range:NSMakeRange(0, tokenPayload.length)];
+
+  // Pad the token payload with "=" signs if the payload's length is not a multiple of 4.
+  while ((tokenPayload.length % 4) != 0) {
+    [tokenPayload appendFormat:@"="];
+  }
+  NSData *decodedTokenPayloadData =
+  [[NSData alloc] initWithBase64EncodedString:tokenPayload
+                                      options:NSDataBase64DecodingIgnoreUnknownCharacters];
+  if (!decodedTokenPayloadData) {
+    return nil;
+  }
+  NSError *jsonError = nil;
+  NSJSONReadingOptions options = NSJSONReadingMutableContainers|NSJSONReadingAllowFragments;
+  NSDictionary *tokenPayloadDictionary =
+  [NSJSONSerialization JSONObjectWithData:decodedTokenPayloadData
+                                  options:options
+                                    error:&jsonError];
+  if (jsonError != nil) {
+    return nil;
+  }
+
+  if (!tokenPayloadDictionary) {
+    return nil;
+  }
+
+  // These are dates since 00:00:00 January 1 1970, as described by the Terminology section in
+  // the JWT spec. https://tools.ietf.org/html/rfc7519
+  NSDate *expirationDate =
+  [NSDate dateWithTimeIntervalSince1970:[tokenPayloadDictionary[@"exp"] doubleValue]];
+  NSDate *authDate =
+  [NSDate dateWithTimeIntervalSince1970:[tokenPayloadDictionary[@"auth_time"] doubleValue]];
+  NSDate *issuedAtDate =
+  [NSDate dateWithTimeIntervalSince1970:[tokenPayloadDictionary[@"iat"] doubleValue]];
+
+  NSDictionary *firebaseTokenPayloadDictionary = tokenPayloadDictionary[@"firebase"];
+  NSString *signInProvider = firebaseTokenPayloadDictionary[@"sign_in_provider"];
+  NSString *signInSecondFactor = firebaseTokenPayloadDictionary[@"sign_in_second_factor"];
+
+  FIRAuthTokenResult *tokenResult = [[FIRAuthTokenResult alloc] initWithToken:token
+                                                               expirationDate:expirationDate
+                                                                     authDate:authDate
+                                                                 issuedAtDate:issuedAtDate
+                                                               signInProvider:signInProvider
+                                                           signInSecondFactor:signInSecondFactor
+                                                                       claims:tokenPayloadDictionary];
+  return tokenResult;
+}
+
 #pragma mark - NSSecureCoding
 
 + (BOOL)supportsSecureCoding {
@@ -75,34 +153,12 @@ static NSString *const kClaimsKey = @"claims";
 }
 
 - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
-  NSString *token =
-      [aDecoder decodeObjectOfClass:[NSDate class] forKey:kTokenKey];
-  NSDate *expirationDate =
-      [aDecoder decodeObjectOfClass:[NSDate class] forKey:kExpirationDateKey];
-  NSDate *authDate =
-      [aDecoder decodeObjectOfClass:[NSDate class] forKey:kAuthDateKey];
-  NSDate *issuedAtDate =
-      [aDecoder decodeObjectOfClass:[NSDate class] forKey:kAuthDateKey];
-  NSString *signInProvider =
-      [aDecoder decodeObjectOfClass:[NSString class] forKey:kSignInProviderKey];
-  NSDictionary<NSString *, NSString *> *claims =
-      [aDecoder decodeObjectOfClass:[NSDictionary<NSString *, NSString *> class] forKey:kClaimsKey];
-
-  return [self initWithToken:token
-              expirationDate:expirationDate
-                    authDate:authDate
-                issuedAtDate:issuedAtDate
-              signInProvider:signInProvider
-                      claims:claims];
+  NSString *token = [aDecoder decodeObjectOfClass:[NSDate class] forKey:kTokenKey];
+  return [FIRAuthTokenResult tokenResultWithToken:token];
 }
 
 - (void)encodeWithCoder:(NSCoder *)aCoder {
   [aCoder encodeObject:_token forKey:kTokenKey];
-  [aCoder encodeObject:_expirationDate forKey:kExpirationDateKey];
-  [aCoder encodeObject:_authDate forKey:kAuthDateKey];
-  [aCoder encodeObject:_issuedAtDate forKey:kIssuedDateKey];
-  [aCoder encodeObject:_signInProvider forKey:kSignInProviderKey];
-  [aCoder encodeObject:_claims forKey:kClaimsKey];
 }
 
 @end

+ 6 - 6
Firebase/Auth/Source/Auth/FIRAuthTokenResult_Internal.h

@@ -25,12 +25,12 @@
  */
 @interface FIRAuthTokenResult () <NSSecureCoding>
 
-- (instancetype)initWithToken:(NSString *)token
-               expirationDate:(NSDate *)expirationDate
-                     authDate:(NSDate *)authDate
-                 issuedAtDate:(NSDate *)issuedAtDate
-               signInProvider:(NSString *)signInProvider
-                       claims:(NSDictionary *)claims;
+/** @fn tokenResultWithToken:
+    @brief Parse a token string to a structured token.
+    @param token The token string to parse.
+    @return A structured token result.
+*/
++ (nullable FIRAuthTokenResult *)tokenResultWithToken:(NSString *)token;
 
 @end
 

+ 26 - 0
Firebase/Auth/Source/Auth/FIRAuth_Internal.h

@@ -117,6 +117,32 @@ NS_ASSUME_NONNULL_BEGIN
  */
 - (BOOL)signOutByForceWithUserID:(NSString *)userID error:(NSError *_Nullable *_Nullable)error;
 
+/** @fn completeSignInWithTokenService:callback:
+    @brief Completes a sign-in flow once we have access and refresh tokens for the user.
+    @param accessToken The STS access token.
+    @param accessTokenExpirationDate The approximate expiration date of the access token.
+    @param refreshToken The STS refresh token.
+    @param anonymous Whether or not the user is anonymous.
+    @param callback Called when the user has been signed in or when an error occurred. Invoked
+        asynchronously on the global auth work queue in the future.
+*/
+- (void)completeSignInWithAccessToken:(nullable NSString *)accessToken
+            accessTokenExpirationDate:(nullable NSDate *)accessTokenExpirationDate
+                         refreshToken:(nullable NSString *)refreshToken
+                            anonymous:(BOOL)anonymous
+                             callback:(FIRAuthResultCallback)callback;
+
+/** @fn signInFlowAuthResultCallbackByDecoratingCallback:
+    @brief Creates a FIRAuthResultCallback block which wraps another FIRAuthResultCallback; trying
+        to update the current user before forwarding it's invocations along to a subject block
+    @param callback Called when the user has been updated or when an error has occurred. Invoked
+        asynchronously on the main thread in the future.
+    @return Returns a block that updates the current user.
+    @remarks Typically invoked as part of the complete sign-in flow. For any other uses please
+        consider alternative ways of updating the current user.
+*/
+- (FIRAuthDataResultCallback)signInFlowAuthDataResultCallbackByDecoratingCallback:(nullable FIRAuthDataResultCallback)callback;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 5 - 2
Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthCredential.m

@@ -15,15 +15,18 @@
  */
 
 #include <TargetConditionals.h>
-#if !TARGET_OS_OSX && !TARGET_OS_TV
+#if TARGET_OS_IOS
 
 #import "FIRPhoneAuthCredential.h"
 
-#import "FIRPhoneAuthCredential_Internal.h"
 #import "FIRAuthCredential_Internal.h"
 #import "FIRAuthExceptionUtils.h"
 #import "FIRVerifyAssertionRequest.h"
 
+#if TARGET_OS_IOS
+#import "FIRPhoneAuthCredential_Internal.h"
+#endif
+
 NS_ASSUME_NONNULL_BEGIN
 
 @interface FIRPhoneAuthCredential ()

+ 1 - 1
Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthCredential_Internal.h

@@ -15,7 +15,7 @@
  */
 
 #include <TargetConditionals.h>
-#if !TARGET_OS_OSX && !TARGET_OS_TV
+#if TARGET_OS_IOS
 
 #import <Foundation/Foundation.h>
 

+ 216 - 9
Firebase/Auth/Source/AuthProvider/Phone/FIRPhoneAuthProvider.m

@@ -15,33 +15,44 @@
  */
 
 #include <TargetConditionals.h>
-#if !TARGET_OS_OSX && !TARGET_OS_TV
+#if TARGET_OS_IOS
 
 #import "FIRPhoneAuthProvider.h"
 
-#import <FirebaseCore/FIRLogger.h>
-#import "FIRPhoneAuthCredential_Internal.h"
 #import <FirebaseCore/FIRApp.h>
+#import <FirebaseCore/FIRLogger.h>
+#import <FirebaseCore/FIROptions.h>
+
 #import "FIRAuthAPNSToken.h"
 #import "FIRAuthAPNSTokenManager.h"
 #import "FIRAuthAppCredential.h"
 #import "FIRAuthAppCredentialManager.h"
+#import "FIRAuthBackend+MultiFactor.h"
+#import "FIRAuthBackend.h"
+#import "FIRAuthErrorUtils.h"
 #import "FIRAuthGlobalWorkQueue.h"
-#import "FIRAuth_Internal.h"
-#import "FIRAuthURLPresenter.h"
 #import "FIRAuthNotificationManager.h"
-#import "FIRAuthErrorUtils.h"
-#import "FIRAuthBackend.h"
+#import "FIRAuthProtoStartMFAPhoneRequestInfo.h"
 #import "FIRAuthSettings.h"
+#import "FIRAuthURLPresenter.h"
 #import "FIRAuthWebUtils.h"
-#import "FirebaseAuthVersion.h"
-#import <FirebaseCore/FIROptions.h>
+#import "FIRAuth_Internal.h"
 #import "FIRGetProjectConfigRequest.h"
 #import "FIRGetProjectConfigResponse.h"
+#import "FIRMultiFactorResolver.h"
+#import "FIRMultiFactorSession+Internal.h"
 #import "FIRSendVerificationCodeRequest.h"
 #import "FIRSendVerificationCodeResponse.h"
+#import "FIRStartMFAEnrollmentRequest.h"
+#import "FIRStartMFAEnrollmentResponse.h"
 #import "FIRVerifyClientRequest.h"
 #import "FIRVerifyClientResponse.h"
+#import "FirebaseAuthVersion.h"
+
+#if TARGET_OS_IOS
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "FIRPhoneMultiFactorInfo+Internal.h"
+#endif
 
 NS_ASSUME_NONNULL_BEGIN
 
@@ -80,6 +91,8 @@ static NSString *const kAuthTypeVerifyApp = @"verifyApp";
  */
 NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
 
+extern NSString *const FIRPhoneMultiFactorID;
+
 @implementation FIRPhoneAuthProvider {
 
   /** @var _auth
@@ -140,6 +153,56 @@ NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
   });
 }
 
+- (void)verifyPhoneNumberWithMultiFactorInfo:(FIRPhoneMultiFactorInfo *)phoneMultiFactorInfo
+                                  UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+                          multiFactorSession:(nullable FIRMultiFactorSession *)session
+                                  completion:(nullable FIRVerificationResultCallback)completion {
+  session.multiFactorInfo = phoneMultiFactorInfo;
+  [self verifyPhoneNumber:phoneMultiFactorInfo.phoneNumber
+               UIDelegate:UIDelegate
+       multiFactorSession:session
+               completion:completion];
+}
+
+- (void)verifyPhoneNumber:(NSString *)phoneNumber
+               UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+       multiFactorSession:(nullable FIRMultiFactorSession *)session
+               completion:(nullable FIRVerificationResultCallback)completion {
+  if (!session) {
+    [self verifyPhoneNumber:phoneNumber UIDelegate:UIDelegate completion:completion];
+    return;
+  }
+
+  if (![FIRAuthWebUtils isCallbackSchemeRegisteredForCustomURLScheme:_callbackScheme]) {
+    [NSException raise:NSInternalInconsistencyException
+                format:@"Please register custom URL scheme '%@' in the app's Info.plist file.",
+     _callbackScheme];
+  }
+  dispatch_async(FIRAuthGlobalWorkQueue(), ^{
+    FIRVerificationResultCallback callBackOnMainThread = ^(NSString *_Nullable verificationID,
+                                                           NSError *_Nullable error) {
+      if (completion) {
+        dispatch_async(dispatch_get_main_queue(), ^{
+          completion(verificationID, error);
+        });
+      }
+    };
+    [self internalVerifyPhoneNumber:phoneNumber
+                         UIDelegate:UIDelegate
+                 multiFactorSession:session
+                         completion:^(NSString *_Nullable verificationID,
+                                      NSError *_Nullable error) {
+      if (!error) {
+       callBackOnMainThread(verificationID, nil);
+       return;
+      } else {
+       callBackOnMainThread(nil, error);
+       return;
+      }
+    }];
+  });
+}
+
 - (FIRPhoneAuthCredential *)credentialWithVerificationID:(NSString *)verificationID
                                         verificationCode:(NSString *)verificationCode {
   return [[FIRPhoneAuthCredential alloc] initWithProviderID:FIRPhoneAuthProviderID
@@ -236,6 +299,38 @@ NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
   }];
 }
 
+- (void)internalVerifyPhoneNumber:(NSString *)phoneNumber
+                       UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+               multiFactorSession:(nullable FIRMultiFactorSession *)session
+                       completion:(nullable FIRVerificationResultCallback)completion {
+  if (!phoneNumber.length) {
+    if (completion) {
+      completion(nil, [FIRAuthErrorUtils missingPhoneNumberErrorWithMessage:nil]);
+    }
+    return;
+  }
+  [_auth.notificationManager checkNotificationForwardingWithCallback:
+   ^(BOOL isNotificationBeingForwarded) {
+     if (!isNotificationBeingForwarded) {
+       if (completion) {
+         completion(nil, [FIRAuthErrorUtils notificationNotForwardedError]);
+       }
+       return;
+     }
+     FIRVerificationResultCallback callback = ^(NSString *_Nullable verificationID,
+                                                NSError *_Nullable error) {
+       if (completion) {
+         completion(verificationID, error);
+       }
+     };
+     [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+                                retryOnInvalidAppCredential:YES
+                                                 UIDelegate:UIDelegate
+                                         multiFactorSession:session
+                                                   callback:callback];
+   }];
+}
+
 /** @fn verifyClientAndSendVerificationCodeToPhoneNumber:retryOnInvalidAppCredential:callback:
     @brief Starts the flow to verify the client via silent push notification.
     @param retryOnInvalidAppCredential Whether of not the flow should be retried if an
@@ -313,6 +408,118 @@ NSString *const kReCAPTCHAURLStringFormat = @"https://%@/__/auth/handler?";
   }];
 }
 
+- (void)verifyClientAndSendVerificationCodeToPhoneNumber:(NSString *)phoneNumber
+                             retryOnInvalidAppCredential:(BOOL)retryOnInvalidAppCredential
+                                              UIDelegate:(nullable id<FIRAuthUIDelegate>)UIDelegate
+                                      multiFactorSession:(nullable FIRMultiFactorSession *)session
+                                                callback:(FIRVerificationResultCallback)callback {
+  if (_auth.settings.isAppVerificationDisabledForTesting) {
+    FIRSendVerificationCodeRequest *request =
+        [[FIRSendVerificationCodeRequest alloc] initWithPhoneNumber:phoneNumber
+                                                      appCredential:nil
+                                                     reCAPTCHAToken:nil
+                                               requestConfiguration:_auth.requestConfiguration];
+    [FIRAuthBackend sendVerificationCode:request
+                                callback:^(FIRSendVerificationCodeResponse *_Nullable response,
+                                           NSError *_Nullable error) {
+                                  callback(response.verificationID, error);
+                                }];
+    return;
+  }
+
+  [self verifyClientWithUIDelegate:UIDelegate
+                        completion:^(FIRAuthAppCredential *_Nullable appCredential,
+                                     NSString *_Nullable reCAPTCHAToken,
+                                     NSError *_Nullable error) {
+    if (error) {
+      if (callback) {
+        callback(nil, error);
+      }
+      return;
+    }
+
+    NSString *IDToken = session.IDToken;
+    NSString *multiFactorProvider = FIRPhoneMultiFactorID;
+    FIRAuthProtoStartMFAPhoneRequestInfo *startMFARequestInfo =
+        [[FIRAuthProtoStartMFAPhoneRequestInfo alloc] initWithPhoneNumber:phoneNumber
+                                                            appCredential:appCredential
+                                                           reCAPTCHAToken:reCAPTCHAToken];
+    if (session.IDToken) {
+      FIRStartMFAEnrollmentRequest *request =
+      [[FIRStartMFAEnrollmentRequest alloc] initWithIDToken:IDToken
+                                        multiFactorProvider:multiFactorProvider
+                                             enrollmentInfo:startMFARequestInfo
+                                       requestConfiguration:self->_auth.requestConfiguration];
+      [FIRAuthBackend startMultiFactorEnrollment:request
+                                        callback:^(FIRStartMFAEnrollmentResponse * _Nullable response,
+                                                   NSError * _Nullable error) {
+        if (error) {
+          if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
+            if (retryOnInvalidAppCredential) {
+              [self->_auth.appCredentialManager clearCredential];
+              [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+                                         retryOnInvalidAppCredential:NO
+                                                          UIDelegate:UIDelegate
+                                                  multiFactorSession:session
+                                                            callback:callback];
+              return;
+            }
+            if (callback) {
+              callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
+                                                                          underlyingError:error]);
+            }
+            return;
+          } else {
+            if (callback) {
+              callback(nil, error);
+            }
+          }
+        } else {
+          if (callback) {
+            callback(response.enrollmentResponse.sessionInfo, nil);
+          }
+        }
+      }];
+    } else {
+      FIRStartMFASignInRequest *request =
+      [[FIRStartMFASignInRequest alloc] initWithMFAProvider:multiFactorProvider
+                                       MFAPendingCredential:session.MFAPendingCredential
+                                            MFAEnrollmentID:session.multiFactorInfo.UID
+                                                 signInInfo:startMFARequestInfo
+                                       requestConfiguration:self->_auth.requestConfiguration];
+      [FIRAuthBackend startMultiFactorSignIn:request
+                                    callback:^(FIRStartMFASignInResponse * _Nullable response, NSError * _Nullable error) {
+        if (error) {
+          if (error.code == FIRAuthErrorCodeInvalidAppCredential) {
+            if (retryOnInvalidAppCredential) {
+              [self->_auth.appCredentialManager clearCredential];
+              [self verifyClientAndSendVerificationCodeToPhoneNumber:phoneNumber
+                                         retryOnInvalidAppCredential:NO
+                                                          UIDelegate:UIDelegate
+                                                  multiFactorSession:session
+                                                            callback:callback];
+              return;
+            }
+            if (callback) {
+              callback(nil, [FIRAuthErrorUtils unexpectedResponseWithDeserializedResponse:nil
+                                                                          underlyingError:error]);
+            }
+            return;
+          } else {
+            if (callback) {
+              callback(nil, error);
+            }
+          }
+        } else {
+          if (callback) {
+            callback(response.responseInfo.sessionInfo, nil);
+          }
+        }
+      }];
+    }
+  }];
+}
+
 /** @fn verifyClientWithCompletion:completion:
     @brief Continues the flow to verify the client via silent push notification.
     @param completion The callback to be invoked when the client verification flow is finished.

+ 126 - 0
Firebase/Auth/Source/Backend/FIRAuthBackend+MultiFactor.h

@@ -0,0 +1,126 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRAuthBackend.h"
+
+#import "FIRStartMFAEnrollmentRequest.h"
+#import "FIRStartMFAEnrollmentResponse.h"
+#import "FIRFinalizeMFAEnrollmentRequest.h"
+#import "FIRFinalizeMFAEnrollmentResponse.h"
+#import "FIRStartMFASignInRequest.h"
+#import "FIRStartMFASignInResponse.h"
+#import "FIRFinalizeMFASignInRequest.h"
+#import "FIRFinalizeMFASignInResponse.h"
+#import "FIRWithdrawMFARequest.h"
+#import "FIRWithdrawMFAResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @typedef FIRStartMFAEnrollmentResponseCallback
+    @brief The type of block used to return the result of a call to the startMFAEnroll endpoint.
+    @param response The received response, if any.
+    @param error The error which occurred, if any.
+    @remarks One of response or error will be non-nil.
+*/
+typedef void (^FIRStartMFAEnrollmentResponseCallback)
+(FIRStartMFAEnrollmentResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRFinalizeMFAEnrollmentResponseCallback
+    @brief The type of block used to return the result of a call to the finalizeMFAEnroll endpoint.
+    @param response The received response, if any.
+    @param error The error which occurred, if any.
+    @remarks One of response or error will be non-nil.
+*/
+typedef void (^FIRFinalizeMFAEnrollmentResponseCallback)
+(FIRFinalizeMFAEnrollmentResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRStartMFASignInResponseCallback
+    @brief The type of block used to return the result of a call to the startMFASignIn endpoint.
+    @param response The received response, if any.
+    @param error The error which occurred, if any.
+    @remarks One of response or error will be non-nil.
+*/
+typedef void (^FIRStartMFASignInResponseCallback)
+(FIRStartMFASignInResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRFinalizeMFASignInResponseCallback
+    @brief The type of block used to return the result of a call to the finalizeMFASignIn endpoint.
+    @param response The received response, if any.
+    @param error The error which occurred, if any.
+    @remarks One of response or error will be non-nil.
+*/
+typedef void (^FIRFinalizeMFASignInResponseCallback)
+(FIRFinalizeMFASignInResponse *_Nullable response, NSError *_Nullable error);
+
+/** @typedef FIRWithdrawMFAResponseCallback
+    @brief The type of block used to return the result of a call to the MFAUnenroll endpoint.
+    @param response The received response, if any.
+    @param error The error which occurred, if any.
+    @remarks One of response or error will be non-nil.
+*/
+typedef void (^FIRWithdrawMFAResponseCallback)
+(FIRWithdrawMFAResponse *_Nullable response, NSError *_Nullable error);
+
+@interface FIRAuthBackend (MultiFactor)
+
+/** @fn startMultiFactorEnrollment:callback:
+    @brief Calls the startMFAEnrollment endpoint.
+    @param request The request parameters.
+    @param callback The callback.
+*/
++ (void)startMultiFactorEnrollment:(FIRStartMFAEnrollmentRequest *)request
+                          callback:(FIRStartMFAEnrollmentResponseCallback)callback;
+
+/** @fn finalizeMultiFactorEnrollment:callback:
+    @brief Calls the finalizeMultiFactorEnrollment endpoint.
+    @param request The request parameters.
+    @param callback The callback.
+*/
++ (void)finalizeMultiFactorEnrollment:(FIRFinalizeMFAEnrollmentRequest *)request
+                             callback:(FIRFinalizeMFAEnrollmentResponseCallback)callback;
+
+/** @fn startMultiFactorSignIn:callback:
+    @brief Calls the startMultiFactorSignIn endpoint.
+    @param request The request parameters.
+    @param callback The callback.
+*/
++ (void)startMultiFactorSignIn:(FIRStartMFASignInRequest *)request
+                      callback:(FIRStartMFASignInResponseCallback)callback;
+
+/** @fn finalizeMultiFactorSignIn:callback:
+    @brief Calls the finalizeMultiFactorSignIn endpoint.
+    @param request The request parameters.
+    @param callback The callback.
+*/
++ (void)finalizeMultiFactorSignIn:(FIRFinalizeMFASignInRequest *)request
+                         callback:(FIRFinalizeMFASignInResponseCallback)callback;
+
+/** @fn withdrawMultiFactor:callback:
+    @brief Calls the withdrawMultiFactor endpoint.
+    @param request The request parameters.
+    @param callback The callback.
+*/
++ (void)withdrawMultiFactor:(FIRWithdrawMFARequest *)request
+                   callback:(FIRWithdrawMFAResponseCallback)callback;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 86 - 0
Firebase/Auth/Source/Backend/FIRAuthBackend+MultiFactor.m

@@ -0,0 +1,86 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRAuthBackend+MultiFactor.h"
+
+@implementation FIRAuthBackend (MultiFactor)
+
++ (void)startMultiFactorEnrollment:(FIRStartMFAEnrollmentRequest *)request
+                          callback:(FIRStartMFAEnrollmentResponseCallback)callback {
+  FIRStartMFAEnrollmentResponse *response = [[FIRStartMFAEnrollmentResponse alloc] init];
+  [[self implementation] postWithRequest:request response:response callback:^(NSError *error) {
+    if (error) {
+      callback(nil, error);
+    } else {
+      callback(response, nil);
+    }
+  }];
+}
+
++ (void)finalizeMultiFactorEnrollment:(FIRFinalizeMFAEnrollmentRequest *)request
+                             callback:(FIRFinalizeMFAEnrollmentResponseCallback)callback {
+  FIRFinalizeMFAEnrollmentResponse *response = [[FIRFinalizeMFAEnrollmentResponse alloc] init];
+  [[self implementation] postWithRequest:request response:response callback:^(NSError *error) {
+    if (error) {
+      callback(nil, error);
+    } else {
+      callback(response, nil);
+    }
+  }];
+}
+
++ (void)startMultiFactorSignIn:(FIRStartMFASignInRequest *)request
+                      callback:(FIRStartMFASignInResponseCallback)callback {
+  FIRStartMFASignInResponse *response = [[FIRStartMFASignInResponse alloc] init];
+  [[self implementation] postWithRequest:request response:response callback:^(NSError *error) {
+    if (error) {
+      callback(nil, error);
+    } else {
+      callback(response, nil);
+    }
+  }];
+}
+
++ (void)finalizeMultiFactorSignIn:(FIRFinalizeMFASignInRequest *)request
+                         callback:(FIRFinalizeMFASignInResponseCallback)callback {
+  FIRFinalizeMFASignInResponse *response = [[FIRFinalizeMFASignInResponse alloc] init];
+  [[self implementation] postWithRequest:request response:response callback:^(NSError *error) {
+    if (error) {
+      callback(nil, error);
+    } else {
+      callback(response, nil);
+    }
+  }];
+}
+
++ (void)withdrawMultiFactor:(FIRWithdrawMFARequest *)request
+                   callback:(FIRWithdrawMFAResponseCallback)callback {
+  FIRWithdrawMFAResponse *response = [[FIRWithdrawMFAResponse alloc] init];
+  [[self implementation] postWithRequest:request response:response callback:^(NSError *error) {
+    if (error) {
+      callback(nil, error);
+    } else {
+      callback(response, nil);
+    }
+  }];
+}
+
+@end
+
+#endif

+ 22 - 0
Firebase/Auth/Source/Backend/FIRAuthBackend.h

@@ -16,6 +16,9 @@
 
 #import <Foundation/Foundation.h>
 
+#import "FIRAuthRPCRequest.h"
+#import "FIRAuthRPCResponse.h"
+
 @class FIRAuthRequestConfiguration;
 @class FIRCreateAuthURIRequest;
 @class FIRCreateAuthURIResponse;
@@ -240,6 +243,8 @@ typedef void (^FIRSignInWithGameCenterResponseCallback)
  */
 + (NSString *)authUserAgent;
 
++ (id<FIRAuthBackendImplementation>)implementation;
+
 /** @fn setBackendImplementation:
     @brief Changes the default backend implementation to something else.
     @param backendImplementation The backend implementation to use.
@@ -594,6 +599,23 @@ typedef void (^FIRSignInWithGameCenterResponseCallback)
 - (void)resetPassword:(FIRResetPasswordRequest *)request
              callback:(FIRResetPasswordCallback)callback;
 
+/** @fn postWithRequest:response:callback:
+    @brief Calls the RPC using HTTP POST.
+    @remarks Possible error responses:
+        @see FIRAuthInternalErrorCodeRPCRequestEncodingError
+        @see FIRAuthInternalErrorCodeJSONSerializationError
+        @see FIRAuthInternalErrorCodeNetworkError
+        @see FIRAuthInternalErrorCodeUnexpectedErrorResponse
+        @see FIRAuthInternalErrorCodeUnexpectedResponse
+        @see FIRAuthInternalErrorCodeRPCResponseDecodingError
+    @param request The request.
+    @param response The empty response to be filled.
+    @param callback The callback for both success and failure.
+*/
+- (void)postWithRequest:(id<FIRAuthRPCRequest>)request
+               response:(id<FIRAuthRPCResponse>)response
+               callback:(void (^)(NSError * _Nullable error))callback;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 134 - 7
Firebase/Auth/Source/Backend/FIRAuthBackend.m

@@ -22,8 +22,6 @@
 #import "FIRAuthErrorUtils.h"
 #import "FIRAuthGlobalWorkQueue.h"
 #import "FirebaseAuth.h"
-#import "FIRAuthRPCRequest.h"
-#import "FIRAuthRPCResponse.h"
 #import "FIRCreateAuthURIRequest.h"
 #import "FIRCreateAuthURIResponse.h"
 #import "FIRDeleteAccountRequest.h"
@@ -36,6 +34,7 @@
 #import "FIRGetOOBConfirmationCodeResponse.h"
 #import "FIRGetProjectConfigRequest.h"
 #import "FIRGetProjectConfigResponse.h"
+#import "FIROAuthCredential_Internal.h"
 #import "FIRResetPasswordRequest.h"
 #import "FIRResetPasswordResponse.h"
 #import "FIRSendVerificationCodeRequest.h"
@@ -59,10 +58,10 @@
 #import "FIRVerifyPhoneNumberRequest.h"
 #import "FIRVerifyPhoneNumberResponse.h"
 
-#import "FIROAuthCredential_Internal.h"
 #if TARGET_OS_IOS
 #import "FIRPhoneAuthCredential_Internal.h"
 #import "FIRPhoneAuthProvider.h"
+#import "FIRPhoneMultiFactorInfo+Internal.h"
 #endif
 
 NS_ASSUME_NONNULL_BEGIN
@@ -164,7 +163,7 @@ static NSString *const kUserTokenExpiredErrorMessage = @"TOKEN_EXPIRED";
  */
 static NSString *const kTooManyRequestsErrorMessage = @"TOO_MANY_ATTEMPTS_TRY_LATER";
 
-/** @var kInvalidTokenCustomErrorMessage
+/** @var kInvalidCustomTokenErrorMessage
     @brief This is the error message the server will respond with if there is a validation error
         with the custom token.
  */
@@ -390,6 +389,56 @@ static NSString *const kMissingClientIdentifier = @"MISSING_CLIENT_IDENTIFIER";
  */
 static NSString *const kCaptchaCheckFailedErrorMessage = @"CAPTCHA_CHECK_FAILED";
 
+/** @var kMissingMFAPendingCredentialErrorMessage
+ @brief This is the error message the server will respond with if the MFA pending credential is missing.
+ */
+static NSString *const kMissingMFAPendingCredentialErrorMessage = @"MISSING_MFA_PENDING_CREDENTIAL";
+
+/** @var kMissingMFAEnrollmentIDErrorMessage
+ @brief This is the error message the server will respond with if the MFA enrollment ID is missing.
+ */
+static NSString *const kMissingMFAEnrollmentIDErrorMessage = @"MISSING_MFA_ENROLLMENT_ID";
+
+/** @var kInvalidMFAPendingCredentialErrorMessage
+ @brief This is the error message the server will respond with if the MFA pending credential is invalid.
+ */
+static NSString *const kInvalidMFAPendingCredentialErrorMessage = @"INVALID_MFA_PENDING_CREDENTIAL";
+
+/** @var kMFAEnrollmentNotFoundErrorMessage
+ @brief This is the error message the server will respond with if the MFA enrollment info is not found.
+ */
+static NSString *const kMFAEnrollmentNotFoundErrorMessage = @"MFA_ENROLLMENT_NOT_FOUND";
+
+/** @var kAdminOnlyOperationErrorMessage
+ @brief This is the error message the server will respond with if the operation is admin only.
+ */
+static NSString *const kAdminOnlyOperationErrorMessage = @"ADMIN_ONLY_OPERATION";
+
+/** @var kUnverifiedEmailErrorMessage
+ @brief This is the error message the server will respond with if the email is unverified.
+ */
+static NSString *const kUnverifiedEmailErrorMessage = @"UNVERIFIED_EMAIL";
+
+/** @var kSecondFactorExistsErrorMessage
+ @brief This is the error message the server will respond with if the second factor already exsists.
+ */
+static NSString *const kSecondFactorExistsErrorMessage = @"SECOND_FACTOR_EXISTS";
+
+/** @var kSecondFactorLimitExceededErrorMessage
+ @brief This is the error message the server will respond with if the number of second factor reaches the limit.
+ */
+static NSString *const kSecondFactorLimitExceededErrorMessage = @"SECOND_FACTOR_LIMIT_EXCEEDED";
+
+/** @var kUnsupportedFirstFactorErrorMessage
+ @brief This is the error message the server will respond with if the first factor doesn't support MFA.
+ */
+static NSString *const kUnsupportedFirstFactorErrorMessage = @"UNSUPPORTED_FIRST_FACTOR";
+
+/** @var kEmailChangeNeedsVerificationErrorMessage
+ @brief This is the error message the server will respond with if changing an unverified email.
+ */
+static NSString *const kEmailChangeNeedsVerificationErrorMessage = @"EMAIL_CHANGE_NEEDS_VERIFICATION";
+
 /** @var kInvalidPendingToken
     @brief Generic IDP error codes.
  */
@@ -648,9 +697,23 @@ static id<FIRAuthBackendImplementation> gBackendImplementation;
   [self postWithRequest:request response:response callback:^(NSError *error) {
     if (error) {
       callback(nil, error);
-      return;
+    } else {
+      if (!response.IDToken && response.MFAInfo) {
+#if TARGET_OS_IOS
+        NSMutableArray<FIRMultiFactorInfo *> *multiFactorInfo = [NSMutableArray array];
+        for (FIRAuthProtoMFAEnrollment *MFAEnrollment in response.MFAInfo) {
+          FIRPhoneMultiFactorInfo *info = [[FIRPhoneMultiFactorInfo alloc] initWithProto:MFAEnrollment];
+          [multiFactorInfo addObject:info];
+        }
+        NSError *multiFactorRequiredError =
+        [FIRAuthErrorUtils secondFactorRequiredErrorWithPendingCredential:response.MFAPendingCredential
+                                                                    hints:multiFactorInfo];
+        callback(nil, multiFactorRequiredError);
+        #endif
+      } else {
+        callback(response, nil);
+      }
     }
-    callback(response, nil);
   }];
 }
 
@@ -673,7 +736,21 @@ static id<FIRAuthBackendImplementation> gBackendImplementation;
     if (error) {
       callback(nil, error);
     } else {
-      callback(response, nil);
+      if (!response.IDToken && response.MFAInfo) {
+        #if TARGET_OS_IOS
+        NSMutableArray<FIRMultiFactorInfo *> *multiFactorInfo = [NSMutableArray array];
+        for (FIRAuthProtoMFAEnrollment *MFAEnrollment in response.MFAInfo) {
+          FIRPhoneMultiFactorInfo *info = [[FIRPhoneMultiFactorInfo alloc] initWithProto:MFAEnrollment];
+          [multiFactorInfo addObject:info];
+        }
+        NSError *multiFactorRequiredError =
+            [FIRAuthErrorUtils secondFactorRequiredErrorWithPendingCredential:response.MFAPendingCredential
+                                                                        hints:multiFactorInfo];
+        callback(nil, multiFactorRequiredError);
+        #endif
+      } else {
+        callback(response, nil);
+      }
     }
   }];
 }
@@ -1185,6 +1262,56 @@ static id<FIRAuthBackendImplementation> gBackendImplementation;
     return [FIRAuthErrorUtils missingOrInvalidNonceErrorWithMessage:serverDetailErrorMessage];
   }
 
+  if ([shortErrorMessage isEqualToString:kMissingMFAPendingCredentialErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeMissingMultiFactorSession
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kMissingMFAEnrollmentIDErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeMissingMultiFactorInfo
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kInvalidMFAPendingCredentialErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeInvalidMultiFactorSession
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kMFAEnrollmentNotFoundErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeMultiFactorInfoNotFound
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kAdminOnlyOperationErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeAdminRestrictedOperation
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kUnverifiedEmailErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeUnverifiedEmail
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kSecondFactorExistsErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeSecondFactorAlreadyEnrolled
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kSecondFactorLimitExceededErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeMaximumSecondFactorCountExceeded
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kUnsupportedFirstFactorErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeUnsupportedFirstFactor
+                                    message:serverErrorMessage];
+  }
+
+  if ([shortErrorMessage isEqualToString:kEmailChangeNeedsVerificationErrorMessage]) {
+    return [FIRAuthErrorUtils errorWithCode:FIRAuthInternalErrorCodeEmailChangeNeedsVerification
+                                    message:serverErrorMessage];
+  }
+
   // In this case we handle an error that might be specified in the underlying errors dictionary,
   // the error message in determined based on the @c reason key in the dictionary.
   if (errorDictionary[kErrorsKey]) {

+ 6 - 1
Firebase/Auth/Source/Backend/FIRIdentityToolkitRequest.h

@@ -49,7 +49,12 @@ NS_ASSUME_NONNULL_BEGIN
  */
 - (nullable instancetype)initWithEndpoint:(NSString *)endpoint
                      requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration
-                         NS_DESIGNATED_INITIALIZER;
+NS_DESIGNATED_INITIALIZER;
+
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+                     requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration
+                      useIdentityPlatform:(BOOL)useIdentityPlatform
+                               useStaging:(BOOL)useStaging;
 
 /** @fn requestURL
     @brief Gets the request's full URL.

+ 46 - 9
Firebase/Auth/Source/Backend/FIRIdentityToolkitRequest.m

@@ -18,27 +18,47 @@
 
 NS_ASSUME_NONNULL_BEGIN
 
-/** @var kAPIURLFormat
-    @brief URL format for server API calls.
- */
-static NSString *const kAPIURLFormat = @"https://%@/identitytoolkit/v3/relyingparty/%@?key=%@";
+static NSString *const kFirebaseAuthAPIURLFormat = @"https://%@/identitytoolkit/v3/relyingparty/%@?key=%@";
+static NSString *const kIdentityPlatformAPIURLFormat = @"https://%@/v2/%@?key=%@";
 
-/** @var gAPIHost
-    @brief Host for server API calls.
- */
 static NSString *gAPIHost = @"www.googleapis.com";
 
+static NSString *kFirebaseAuthAPIHost = @"www.googleapis.com";
+static NSString *kIdentityPlatformAPIHost = @"identitytoolkit.googleapis.com";
+
+static NSString *kFirebaseAuthStagingAPIHost = @"staging-www.sandbox.googleapis.com";
+static NSString *kIdentityPlatformStagingAPIHost = @"staging-identitytoolkit.sandbox.googleapis.com";
+
 @implementation FIRIdentityToolkitRequest {
   FIRAuthRequestConfiguration *_requestConfiguration;
+
+  BOOL _useIdentityPlatform;
+
+  BOOL _useStaging;
 }
 
 - (nullable instancetype)initWithEndpoint:(NSString *)endpoint
                      requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
   self = [super init];
   if (self) {
-    _endpoint = [endpoint copy];
     _APIKey = [requestConfiguration.APIKey copy];
+    _endpoint = [endpoint copy];
     _requestConfiguration = requestConfiguration;
+    _useIdentityPlatform = NO;
+    _useStaging = NO;
+  }
+  return self;
+}
+
+- (nullable instancetype)initWithEndpoint:(NSString *)endpoint
+                     requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration
+                      useIdentityPlatform:(BOOL)useIdentityPlatform
+                               useStaging:(BOOL)useStaging {
+  self = [self initWithEndpoint:endpoint
+           requestConfiguration:requestConfiguration];
+  if (self) {
+    _useIdentityPlatform = useIdentityPlatform;
+    _useStaging = useStaging;
   }
   return self;
 }
@@ -48,7 +68,24 @@ static NSString *gAPIHost = @"www.googleapis.com";
 }
 
 - (NSURL *)requestURL {
-  NSString *URLString = [NSString stringWithFormat:kAPIURLFormat, gAPIHost, _endpoint, _APIKey];
+  NSString *apiURLFormat;
+  NSString *apiHost;
+  if (_useIdentityPlatform) {
+    apiURLFormat = kIdentityPlatformAPIURLFormat;
+    if (_useStaging) {
+      apiHost = kIdentityPlatformStagingAPIHost;
+    } else {
+      apiHost = kIdentityPlatformAPIHost;
+    }
+  } else {
+    apiURLFormat = kFirebaseAuthAPIURLFormat;
+    if (_useStaging) {
+      apiHost = kFirebaseAuthStagingAPIHost;
+    } else {
+      apiHost = kFirebaseAuthAPIHost;
+    }
+  }
+  NSString *URLString = [NSString stringWithFormat:apiURLFormat, apiHost, _endpoint, _APIKey];
   NSURL *URL = [NSURL URLWithString:URLString];
   return URL;
 }

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRCreateAuthURIRequest.m

@@ -91,7 +91,7 @@ static NSString *const kAppIDKey = @"appId";
   if (_appID) {
     postBody[kAppIDKey] = _appID;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRDeleteAccountRequest.m

@@ -61,7 +61,7 @@ static NSString *const kLocalIDKey = @"localId";
   NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
   postBody[kIDTokenKey] = _accessToken;
   postBody[kLocalIDKey] = _localID;
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIREmailLinkSignInRequest.h

@@ -38,7 +38,7 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic, copy, readonly) NSString *oobCode;
 
-/** @property idToken
+/** @property IDToken
     @brief The ID Token code potentially used to complete the email link sign-in flow.
  */
 @property(nonatomic, copy) NSString *IDToken;

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIREmailLinkSignInRequest.m

@@ -66,7 +66,7 @@ static NSString *const kPostBodyKey = @"postBody";
   if (_IDToken) {
     postBody[kIDTokenKey] = _IDToken;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 3 - 2
Firebase/Auth/Source/Backend/RPC/FIRGetAccountInfoResponse.h

@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-#import <Foundation/Foundation.h>
-
+#import "FIRAuthProtoMFAEnrollment.h"
 #import "FIRAuthRPCResponse.h"
 
 NS_ASSUME_NONNULL_BEGIN
@@ -127,6 +126,8 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic, readonly, nullable) NSString *phoneNumber;
 
+@property(nonatomic, strong, readonly, nullable) NSArray<FIRAuthProtoMFAEnrollment *> *MFAEnrollments;
+
 /** @fn init
     @brief Please use initWithDictionary:
  */

+ 9 - 0
Firebase/Auth/Source/Backend/RPC/FIRGetAccountInfoResponse.m

@@ -80,6 +80,15 @@ static NSString *const kErrorKey = @"error";
     _emailVerified = [dictionary[@"emailVerified"] boolValue];
     _passwordHash = [dictionary[@"passwordHash"] copy];
     _phoneNumber = [dictionary[@"phoneNumber"] copy];
+    NSArray<NSDictionary *> *MFAEnrollmentData = dictionary[@"mfaInfo"];
+    if (MFAEnrollmentData) {
+      NSMutableArray<FIRAuthProtoMFAEnrollment *> *MFAEnrollments =
+      [NSMutableArray arrayWithCapacity:MFAEnrollmentData.count];
+      for (NSDictionary *dictionary in MFAEnrollmentData) {
+        [MFAEnrollments addObject: [[FIRAuthProtoMFAEnrollment alloc] initWithDictionary:dictionary]];
+      }
+      _MFAEnrollments = [MFAEnrollments copy];
+    }
   }
   return self;
 }

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRResetPasswordRequest.m

@@ -52,7 +52,7 @@ static NSString *const kCurrentPasswordKey = @"newPassword";
   if (_updatedPassword) {
     postBody[kCurrentPasswordKey] = _updatedPassword;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRSecureTokenRequest.m

@@ -145,7 +145,7 @@ static NSString *gAPIHost = @"securetoken.googleapis.com";
   if (_code) {
     postBody[kCodeKey] = _code;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 #pragma mark - Internal API for development

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRSendVerificationCodeRequest.m

@@ -76,7 +76,7 @@ static NSString *const kreCAPTCHATokenKey = @"recaptchaToken";
   if (_reCAPTCHAToken) {
     postBody[kreCAPTCHATokenKey] = _reCAPTCHAToken;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRSetAccountInfoRequest.m

@@ -171,7 +171,7 @@ static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
   if (_returnSecureToken) {
     postBody[kReturnSecureTokenKey] = @YES;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRSignInWithGameCenterRequest.m

@@ -72,7 +72,7 @@ static NSString *const kSignInWithGameCenterEndPoint = @"signInWithGameCenter";
   if (_displayName) {
     postBody[@"displayName"] = _displayName;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRSignUpNewUserRequest.m

@@ -82,7 +82,7 @@ static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
   if (_returnSecureToken) {
     postBody[kReturnSecureTokenKey] = @YES;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 5 - 2
Firebase/Auth/Source/Backend/RPC/FIRVerifyAssertionResponse.h

@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-#import <Foundation/Foundation.h>
-
+#import "FIRAuthProtoMFAEnrollment.h"
 #import "FIRAuthRPCResponse.h"
 
 NS_ASSUME_NONNULL_BEGIN
@@ -206,6 +205,10 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic, copy, nullable) NSString *pendingToken;
 
+@property(nonatomic, strong, readonly, nullable) NSString *MFAPendingCredential;
+
+@property(nonatomic, strong, readonly, nullable) NSArray<FIRAuthProtoMFAEnrollment *> *MFAInfo;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 12 - 0
Firebase/Auth/Source/Backend/RPC/FIRVerifyAssertionResponse.m

@@ -78,6 +78,18 @@ NS_ASSUME_NONNULL_BEGIN
   _oauthAccessToken = [dictionary[@"oauthAccessToken"] copy];
   _oauthSecretToken = [dictionary[@"oauthTokenSecret"] copy];
   _pendingToken = [dictionary[@"pendingToken"] copy];
+
+  if (dictionary[@"mfaInfo"] != nil) {
+    NSMutableArray<FIRAuthProtoMFAEnrollment *> *MFAInfo = [NSMutableArray array];
+    NSArray *MFAInfoDataArray = dictionary[@"mfaInfo"];
+    for (NSDictionary *MFAInfoData in MFAInfoDataArray) {
+      FIRAuthProtoMFAEnrollment *MFAEnrollment = [[FIRAuthProtoMFAEnrollment alloc] initWithDictionary:MFAInfoData];
+      [MFAInfo addObject:MFAEnrollment];
+    }
+    _MFAInfo = MFAInfo;
+  }
+  _MFAPendingCredential = [dictionary[@"mfaPendingCredential"] copy];
+
   return YES;
 }
 

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRVerifyClientRequest.m

@@ -55,7 +55,7 @@ static NSString *const kIsSandboxKey = @"isSandbox";
   if (_isSandbox) {
     postBody[kIsSandboxKey] = @YES;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordRequest.m

@@ -88,7 +88,7 @@ static NSString *const kReturnSecureTokenKey = @"returnSecureToken";
   if (_returnSecureToken) {
     postBody[kReturnSecureTokenKey] = @YES;
   }
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 5 - 2
Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordResponse.h

@@ -14,8 +14,7 @@
  * limitations under the License.
  */
 
-#import <Foundation/Foundation.h>
-
+#import "FIRAuthProtoMFAEnrollment.h"
 #import "FIRAuthRPCResponse.h"
 
 NS_ASSUME_NONNULL_BEGIN
@@ -67,6 +66,10 @@ NS_ASSUME_NONNULL_BEGIN
  */
 @property(nonatomic, strong, readonly, nullable) NSURL *photoURL;
 
+@property(nonatomic, strong, readonly, nullable) NSString *MFAPendingCredential;
+
+@property(nonatomic, strong, readonly, nullable) NSArray<FIRAuthProtoMFAEnrollment *> *MFAInfo;
+
 @end
 
 NS_ASSUME_NONNULL_END

+ 12 - 0
Firebase/Auth/Source/Backend/RPC/FIRVerifyPasswordResponse.m

@@ -30,6 +30,18 @@ NS_ASSUME_NONNULL_BEGIN
       [NSDate dateWithTimeIntervalSinceNow:[dictionary[@"expiresIn"] doubleValue]] : nil;
   _refreshToken = [dictionary[@"refreshToken"] copy];
   _photoURL = dictionary[@"photoUrl"] ? [NSURL URLWithString:dictionary[@"photoUrl"]] : nil;
+
+  if (dictionary[@"mfaInfo"] != nil) {
+    NSMutableArray<FIRAuthProtoMFAEnrollment *> *MFAInfo = [NSMutableArray array];
+    NSArray *MFAInfoDataArray = dictionary[@"mfaInfo"];
+    for (NSDictionary *MFAInfoData in MFAInfoDataArray) {
+      FIRAuthProtoMFAEnrollment *MFAEnrollment = [[FIRAuthProtoMFAEnrollment alloc] initWithDictionary:MFAInfoData];
+      [MFAInfo addObject:MFAEnrollment];
+    }
+    _MFAInfo = MFAInfo;
+  }
+  _MFAPendingCredential = [dictionary[@"mfaPendingCredential"] copy];
+
   return YES;
 }
 

+ 1 - 1
Firebase/Auth/Source/Backend/RPC/FIRVerifyPhoneNumberRequest.m

@@ -125,7 +125,7 @@ NSString *const FIRAuthOperationString(FIRAuthOperationType operationType) {
   }
   NSString *operation = FIRAuthOperationString(_operation);
   postBody[kOperationKey] = operation;
-  return postBody;
+  return [postBody copy];
 }
 
 @end

+ 41 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentRequest.h

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+#import "FIRAuthProtoFinalizeMFAPhoneRequestInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFinalizeMFAEnrollmentRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAProvider;
+
+@property(nonatomic, copy, readonly, nullable) NSString *displayName;
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoFinalizeMFAPhoneRequestInfo *verificationInfo;
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                             MFAProvider:(NSString *)MFAProvider
+                             displayName:(NSString *)displayName
+                        verificationInfo:(FIRAuthProtoFinalizeMFAPhoneRequestInfo *)verificationInfo
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 60 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentRequest.m

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRFinalizeMFAEnrollmentRequest.h"
+
+static NSString *const kFinalizeMFAEnrollmentEndPoint = @"accounts/mfaEnrollment:finalize";
+
+@implementation FIRFinalizeMFAEnrollmentRequest
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                             MFAProvider:(NSString *)MFAProvider
+                             displayName:(NSString *)displayName
+                        verificationInfo:(FIRAuthProtoFinalizeMFAPhoneRequestInfo *)verificationInfo
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
+  self = [super initWithEndpoint:kFinalizeMFAEnrollmentEndPoint
+            requestConfiguration:requestConfiguration
+             useIdentityPlatform:YES
+                      useStaging:NO];
+  if (self) {
+    _IDToken = IDToken;
+    _MFAProvider = MFAProvider;
+    _displayName = displayName;
+    _verificationInfo = verificationInfo;
+  }
+  return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing  _Nullable *)error {
+  NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+  if (_IDToken) {
+    postBody[@"idToken"] = _IDToken;
+  }
+  if (_MFAProvider) {
+    postBody[@"mfaProvider"] = _MFAProvider;
+  }
+  if (_displayName) {
+    postBody[@"displayName"] = _displayName;
+  }
+  if (_verificationInfo) {
+    if ([_verificationInfo isKindOfClass:[FIRAuthProtoFinalizeMFAPhoneRequestInfo class]]) {
+      postBody[@"phoneVerificationInfo"] = [_verificationInfo dictionary];
+    }
+  }
+  return [postBody copy];
+}
+
+@end

+ 30 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentResponse.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCResponse.h"
+#import "FIRAuthProtoFinalizeMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFinalizeMFAEnrollmentResponse : NSObject <FIRAuthRPCResponse>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 30 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRFinalizeMFAEnrollmentResponse.m

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRFinalizeMFAEnrollmentResponse.h"
+
+#import "FIRAuthProtoFinalizeMFAPhoneResponseInfo.h"
+
+@implementation FIRFinalizeMFAEnrollmentResponse
+
+- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary
+                    error:(NSError *__autoreleasing _Nullable * _Nullable)error {
+  _IDToken = [dictionary[@"idToken"] copy];
+  _refreshToken = [dictionary[@"refreshToken"] copy];
+  return YES;
+}
+
+@end

+ 38 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentRequest.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRAuthProtoStartMFAPhoneRequestInfo.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStartMFAEnrollmentRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *multiFactorProvider;
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoStartMFAPhoneRequestInfo *enrollmentInfo;
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                     multiFactorProvider:(NSString *)multiFactorProvider
+                          enrollmentInfo:(FIRAuthProtoStartMFAPhoneRequestInfo *)enrollmentInfo
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 57 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentRequest.m

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRStartMFAEnrollmentRequest.h"
+
+#import "FIRAuthProtoStartMFAPhoneRequestInfo.h"
+
+static NSString *const kStartMFAEnrollmentEndPoint = @"accounts/mfaEnrollment:start";
+
+@implementation FIRStartMFAEnrollmentRequest
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                     multiFactorProvider:(NSString *)multiFactorProvider
+                          enrollmentInfo:(FIRAuthProtoStartMFAPhoneRequestInfo *)enrollmentInfo
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
+  self = [super initWithEndpoint:kStartMFAEnrollmentEndPoint
+            requestConfiguration:requestConfiguration
+             useIdentityPlatform:YES
+                      useStaging:NO];
+  if (self) {
+    _IDToken = IDToken;
+    _multiFactorProvider = multiFactorProvider;
+    _enrollmentInfo = enrollmentInfo;
+  }
+  return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing  _Nullable *)error {
+  NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+  if (_IDToken) {
+    postBody[@"idToken"] = _IDToken;
+  }
+  if (_multiFactorProvider) {
+    postBody[@"mfaProvider"] = _multiFactorProvider;
+  }
+  if (_enrollmentInfo) {
+    if ([_enrollmentInfo isKindOfClass:[FIRAuthProtoStartMFAPhoneRequestInfo class]]) {
+      postBody[@"phoneEnrollmentInfo"] = [_enrollmentInfo dictionary];
+    }
+  }
+  return [postBody copy];
+}
+
+@end

+ 28 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentResponse.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCResponse.h"
+#import "FIRAuthProtoStartMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStartMFAEnrollmentResponse : NSObject <FIRAuthRPCResponse>
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoStartMFAPhoneResponseInfo *enrollmentResponse;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 34 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Enroll/FIRStartMFAEnrollmentResponse.m

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRStartMFAEnrollmentResponse.h"
+
+#import "FIRAuthProtoStartMFAPhoneResponseInfo.h"
+
+@implementation FIRStartMFAEnrollmentResponse
+
+- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary
+                    error:(NSError *__autoreleasing _Nullable * _Nullable)error {
+  if (dictionary[@"phoneSessionInfo"] != nil) {
+    NSDictionary *data = dictionary[@"phoneSessionInfo"];
+    _enrollmentResponse = [[FIRAuthProtoStartMFAPhoneResponseInfo alloc] initWithDictionary:data];
+  } else {
+    return NO;
+  }
+  return YES;
+}
+
+@end

+ 38 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInRequest.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+#import "FIRAuthProtoFinalizeMFAPhoneRequestInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFinalizeMFASignInRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAProvider;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAPendingCredential;
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoFinalizeMFAPhoneRequestInfo *verificationInfo;
+
+- (nullable instancetype)initWithMFAProvider:(NSString *)MFAProvider
+                        MFAPendingCredential:(NSString *)MFAPendingCredential
+                            verificationInfo:(FIRAuthProtoFinalizeMFAPhoneRequestInfo *)verificationInfo
+                        requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 55 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInRequest.m

@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRFinalizeMFASignInRequest.h"
+
+static NSString *const kFinalizeMFASignInEndPoint = @"accounts/mfaSignIn:finalize";
+
+@implementation FIRFinalizeMFASignInRequest
+
+- (nullable instancetype)initWithMFAProvider:(NSString *)MFAProvider
+                        MFAPendingCredential:(NSString *)MFAPendingCredential
+                            verificationInfo:(FIRAuthProtoFinalizeMFAPhoneRequestInfo *)verificationInfo
+                        requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
+  self = [super initWithEndpoint:kFinalizeMFASignInEndPoint
+            requestConfiguration:requestConfiguration
+             useIdentityPlatform:YES
+                      useStaging:NO];
+  if (self) {
+    _MFAProvider = MFAProvider;
+    _MFAPendingCredential = MFAPendingCredential;
+    _verificationInfo = verificationInfo;
+  }
+  return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing  _Nullable *)error {
+  NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+  if (_MFAProvider) {
+    postBody[@"mfaProvider"] = _MFAProvider;
+  }
+  if (_MFAPendingCredential) {
+    postBody[@"mfaPendingCredential"] = _MFAPendingCredential;
+  }
+  if (_verificationInfo) {
+    if ([_verificationInfo isKindOfClass:[FIRAuthProtoFinalizeMFAPhoneRequestInfo class]]) {
+      postBody[@"phoneVerificationInfo"] = [_verificationInfo dictionary];
+    }
+  }
+  return [postBody copy];
+}
+
+@end

+ 30 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInResponse.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCResponse.h"
+#import "FIRAuthProtoFinalizeMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRFinalizeMFASignInResponse : NSObject <FIRAuthRPCResponse>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 28 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRFinalizeMFASignInResponse.m

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRFinalizeMFASignInResponse.h"
+
+@implementation FIRFinalizeMFASignInResponse
+
+- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary
+                    error:(NSError *__autoreleasing _Nullable * _Nullable)error {
+  _IDToken = [dictionary[@"idToken"] copy];
+  _refreshToken = [dictionary[@"refreshToken"] copy];
+  return YES;
+}
+
+@end

+ 41 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInRequest.h

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+#import "FIRAuthProtoStartMFAPhoneRequestInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStartMFASignInRequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAProvider;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAPendingCredential;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAEnrollmentID;
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoStartMFAPhoneRequestInfo *signInInfo;
+
+- (nullable instancetype)initWithMFAProvider:(NSString *)MFAProvider
+                        MFAPendingCredential:(NSString *)MFAPendingCredential
+                             MFAEnrollmentID:(NSString *)MFAEnrollmentID
+                                  signInInfo:(FIRAuthProtoStartMFAPhoneRequestInfo *)signInInfo
+                        requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 60 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInRequest.m

@@ -0,0 +1,60 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRStartMFASignInRequest.h"
+
+static NSString *const kStartMFASignInEndPoint = @"accounts/mfaSignIn:start";
+
+@implementation FIRStartMFASignInRequest
+
+- (nullable instancetype)initWithMFAProvider:(NSString *)MFAProvider
+                        MFAPendingCredential:(NSString *)MFAPendingCredential
+                             MFAEnrollmentID:(NSString *)MFAEnrollmentID
+                                  signInInfo:(FIRAuthProtoStartMFAPhoneRequestInfo *)signInInfo
+                        requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
+  self = [super initWithEndpoint:kStartMFASignInEndPoint
+            requestConfiguration:requestConfiguration
+             useIdentityPlatform:YES
+                      useStaging:NO];
+  if (self) {
+    _MFAProvider = MFAProvider;
+    _MFAPendingCredential = MFAPendingCredential;
+    _MFAEnrollmentID = MFAEnrollmentID;
+    _signInInfo = signInInfo;
+  }
+  return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing  _Nullable *)error {
+  NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+  if (_MFAProvider) {
+    postBody[@"mfaProvider"] = _MFAProvider;
+  }
+  if (_MFAPendingCredential) {
+    postBody[@"mfaPendingCredential"] = _MFAPendingCredential;
+  }
+  if (_MFAEnrollmentID) {
+    postBody[@"mfaEnrollmentId"] = _MFAEnrollmentID;
+  }
+  if (_signInInfo) {
+    if ([_signInInfo isKindOfClass:[FIRAuthProtoStartMFAPhoneRequestInfo class]]) {
+      postBody[@"phoneSignInInfo"] = [_signInInfo dictionary];
+    }
+  }
+  return [postBody copy];
+}
+
+@end

+ 28 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInResponse.h

@@ -0,0 +1,28 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCResponse.h"
+#import "FIRAuthProtoStartMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRStartMFASignInResponse : NSObject <FIRAuthRPCResponse>
+
+@property(nonatomic, copy, readonly, nullable) FIRAuthProtoStartMFAPhoneResponseInfo *responseInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 32 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/SignIn/FIRStartMFASignInResponse.m

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRStartMFASignInResponse.h"
+
+@implementation FIRStartMFASignInResponse
+
+- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary
+                    error:(NSError *__autoreleasing _Nullable * _Nullable)error {
+  if (dictionary[@"phoneResponseInfo"] != nil) {
+    NSDictionary *data = dictionary[@"phoneResponseInfo"];
+    _responseInfo = [[FIRAuthProtoStartMFAPhoneResponseInfo alloc] initWithDictionary:data];
+  } else {
+    return NO;
+  }
+  return YES;
+}
+
+@end

+ 34 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFARequest.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCRequest.h"
+#import "FIRIdentityToolkitRequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRWithdrawMFARequest : FIRIdentityToolkitRequest <FIRAuthRPCRequest>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAEnrollmentID;
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                         MFAEnrollmentID:(NSString *)MFAEnrollmentID
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 52 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFARequest.m

@@ -0,0 +1,52 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRWithdrawMFARequest.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *const kWithdrawMFAEndPoint = @"accounts/mfaEnrollment:withdraw";
+
+@implementation FIRWithdrawMFARequest
+
+- (nullable instancetype)initWithIDToken:(NSString *)IDToken
+                         MFAEnrollmentID:(NSString *)MFAEnrollmentID
+                    requestConfiguration:(FIRAuthRequestConfiguration *)requestConfiguration {
+  self = [super initWithEndpoint:kWithdrawMFAEndPoint
+            requestConfiguration:requestConfiguration
+             useIdentityPlatform:YES
+                      useStaging:NO];
+  if (self) {
+    _IDToken = IDToken;
+    _MFAEnrollmentID = MFAEnrollmentID;
+  }
+  return self;
+}
+
+- (nullable id)unencodedHTTPRequestBodyWithError:(NSError *__autoreleasing  _Nullable *)error {
+  NSMutableDictionary *postBody = [NSMutableDictionary dictionary];
+  if (_IDToken) {
+    postBody[@"idToken"] = _IDToken;
+  }
+  if (_MFAEnrollmentID) {
+    postBody[@"mfaEnrollmentId"] = _MFAEnrollmentID;
+  }
+  return [postBody copy];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 29 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFAResponse.h

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRAuthRPCResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRWithdrawMFAResponse : NSObject <FIRAuthRPCResponse>
+
+@property(nonatomic, copy, readonly, nullable) NSString *IDToken;
+
+@property(nonatomic, copy, readonly, nullable) NSString *refreshToken;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 32 - 0
Firebase/Auth/Source/Backend/RPC/MultiFactor/Unenroll/FIRWithdrawMFAResponse.m

@@ -0,0 +1,32 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#import "FIRWithdrawMFAResponse.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRWithdrawMFAResponse
+
+- (BOOL)setWithDictionary:(nonnull NSDictionary *)dictionary
+                    error:(NSError *__autoreleasing _Nullable * _Nullable)error {
+  _IDToken = [dictionary[@"idToken"] copy];
+  _refreshToken = [dictionary[@"refreshToken"] copy];
+  return YES;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 30 - 0
Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProto.h

@@ -0,0 +1,30 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@protocol FIRAuthProto <NSObject>
+
+@optional
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary;
+
+- (NSDictionary *)dictionary;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProtoMFAEnrollment.h

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProto.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthProtoMFAEnrollment : NSObject <FIRAuthProto>
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAValue;
+
+@property(nonatomic, copy, readonly, nullable) NSString *MFAEnrollmentID;
+
+@property(nonatomic, copy, readonly, nullable) NSString *displayName;
+
+@property(nonatomic, copy, readonly, nullable) NSDate *enrolledAt;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 43 - 0
Firebase/Auth/Source/Backend/RPC/Proto/FIRAuthProtoMFAEnrollment.m

@@ -0,0 +1,43 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProtoMFAEnrollment.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthProtoMFAEnrollment
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+  self = [super init];
+  if (self) {
+    if (dictionary[@"phoneInfo"]) {
+      _MFAValue = dictionary[@"phoneInfo"];
+    }
+    _MFAEnrollmentID = dictionary[@"mfaEnrollmentId"];
+    _displayName = dictionary[@"displayName"];
+    if ([dictionary[@"enrolledAt"] isKindOfClass:[NSString class]]) {
+      NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init];
+      [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss.SSSZ"];
+      NSDate *date = [dateFormatter dateFromString:dictionary[@"enrolledAt"]];
+      _enrolledAt = date;
+    }
+  }
+  return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 31 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneRequestInfo.h

@@ -0,0 +1,31 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProto.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthProtoFinalizeMFAPhoneRequestInfo : NSObject <FIRAuthProto>
+
+@property(nonatomic, strong, readonly, nullable) NSString *sessionInfo;
+
+@property(nonatomic, strong, readonly, nullable) NSString *code;
+
+- (instancetype)initWithSessionInfo:(NSString *)sessionInfo
+                   verificationCode:(NSString *)verificationCode;
+@end
+
+NS_ASSUME_NONNULL_END

+ 46 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneRequestInfo.m

@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProtoFinalizeMFAPhoneRequestInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthProtoFinalizeMFAPhoneRequestInfo
+
+- (instancetype)initWithSessionInfo:(NSString *)sessionInfo
+                   verificationCode:(NSString *)verificationCode {
+  self = [super init];
+  if (self) {
+    _sessionInfo = sessionInfo;
+    _code = verificationCode;
+  }
+  return self;
+}
+
+- (NSDictionary *)dictionary {
+  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+  if (_sessionInfo) {
+    dict[@"sessionInfo"] = _sessionInfo;
+  }
+  if (_code) {
+    dict[@"code"] = _code;
+  }
+  return [dict copy];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 27 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneResponseInfo.h

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProto.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthProtoFinalizeMFAPhoneResponseInfo : NSObject <FIRAuthProto>
+
+@property(nonatomic, copy, readonly, nullable) NSString *phoneNumber;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoFinalizeMFAPhoneResponseInfo.m

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProtoFinalizeMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthProtoFinalizeMFAPhoneResponseInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+  self = [super init];
+  if (self) {
+    _phoneNumber = [dictionary[@"phoneNumber"] copy];
+  }
+  return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 37 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneRequestInfo.h

@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+
+#import "FIRAuthAppCredential.h"
+#import "FIRAuthProto.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthProtoStartMFAPhoneRequestInfo : NSObject <FIRAuthProto>
+
+@property(nonatomic, strong, readonly, nullable) NSString *phoneNumber;
+
+@property(nonatomic, strong, readonly, nullable) FIRAuthAppCredential *appCredential;
+
+@property(nonatomic, strong, readonly, nullable) NSString *reCAPTCHAToken;
+
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+                               appCredential:(nullable FIRAuthAppCredential *)appCredential
+                              reCAPTCHAToken:(nullable NSString *)reCAPTCHAToken;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 74 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneRequestInfo.m

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProtoStartMFAPhoneRequestInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+/** @var kPhoneNumberKey
+ @brief The key for the Phone Number parameter in the request.
+ */
+static NSString *const kPhoneNumberKey = @"phoneNumber";
+
+/** @var kReceiptKey
+ @brief The key for the receipt parameter in the request.
+ */
+static NSString *const kReceiptKey = @"iosReceipt";
+
+/** @var kSecretKey
+ @brief The key for the Secret parameter in the request.
+ */
+static NSString *const kSecretKey = @"iosSecret";
+
+/** @var kreCAPTCHATokenKey
+ @brief The key for the reCAPTCHAToken parameter in the request.
+ */
+static NSString *const kreCAPTCHATokenKey = @"recaptchaToken";
+
+@implementation FIRAuthProtoStartMFAPhoneRequestInfo
+
+- (nullable instancetype)initWithPhoneNumber:(NSString *)phoneNumber
+                               appCredential:(nullable FIRAuthAppCredential *)appCredential
+                              reCAPTCHAToken:(nullable NSString *)reCAPTCHAToken {
+  self = [super init];
+  if (self) {
+    _phoneNumber = [phoneNumber copy];
+    _appCredential = appCredential;
+    _reCAPTCHAToken = [reCAPTCHAToken copy];
+  }
+  return self;
+}
+
+- (NSDictionary *)dictionary {
+  NSMutableDictionary *dict = [NSMutableDictionary dictionary];
+  if (_phoneNumber) {
+    dict[kPhoneNumberKey] = _phoneNumber;
+  }
+  if (_appCredential.receipt) {
+    dict[kReceiptKey] = _appCredential.receipt;
+  }
+  if (_appCredential.secret) {
+    dict[kSecretKey] = _appCredential.secret;
+  }
+  if (_reCAPTCHAToken) {
+    dict[kreCAPTCHATokenKey] = _reCAPTCHAToken;
+  }
+  return [dict copy];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 27 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneResponseInfo.h

@@ -0,0 +1,27 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProto.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRAuthProtoStartMFAPhoneResponseInfo : NSObject <FIRAuthProto>
+
+@property(nonatomic, copy, readonly, nullable) NSString *sessionInfo;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 33 - 0
Firebase/Auth/Source/Backend/RPC/Proto/Phone/FIRAuthProtoStartMFAPhoneResponseInfo.m

@@ -0,0 +1,33 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#import "FIRAuthProtoStartMFAPhoneResponseInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRAuthProtoStartMFAPhoneResponseInfo
+
+- (instancetype)initWithDictionary:(NSDictionary *)dictionary {
+  self = [super init];
+  if (self) {
+    _sessionInfo = [dictionary[@"sessionInfo"] copy];
+  }
+  return self;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 38 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactor+Internal.h

@@ -0,0 +1,38 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactor.h"
+#import "FIRAuthProtoMFAEnrollment.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRMultiFactor ()
+
+@property(nonatomic, weak) FIRUser *user;
+
+/** @fn initWithMFAEnrollments:
+    @brief Initialize a multi factor instance with a list of MFA enrollments.
+*/
+- (instancetype)initWithMFAEnrollments:(NSArray<FIRAuthProtoMFAEnrollment *> *)MFAEnrollments;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 176 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactor.m

@@ -0,0 +1,176 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactor.h"
+#import "FIRMultiFactor+Internal.h"
+
+#import "FIRAuth_Internal.h"
+#import "FIRAuthBackend+MultiFactor.h"
+#import "FIRAuthDataResult_Internal.h"
+#import "FIRMultiFactorInfo+Internal.h"
+#import "FIRStartMFAEnrollmentRequest.h"
+#import "FIRUser_Internal.h"
+#import "FIRMultiFactorSession+Internal.h"
+
+#if TARGET_OS_IOS
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "FIRPhoneMultiFactorAssertion.h"
+#import "FIRPhoneMultiFactorAssertion+Internal.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+static NSString *kEnrolledFactorsCodingKey = @"enrolledFactors";
+
+static NSString *kUserCodingKey = @"user";
+
+@implementation FIRMultiFactor
+
+- (void)getSessionWithCompletion:(nullable FIRMultiFactorSessionCallback)completion {
+  FIRMultiFactorSession *session = [FIRMultiFactorSession sessionForCurrentUser];
+  if (completion) {
+    completion(session, nil);
+  }
+}
+
+- (void)enrollWithAssertion:(FIRMultiFactorAssertion*)assertion
+                displayName:(nullable NSString *)displayName
+                 completion:(nullable FIRAuthVoidErrorCallback)completion {
+#if TARGET_OS_IOS
+  FIRPhoneMultiFactorAssertion *phoneAssertion = (FIRPhoneMultiFactorAssertion *)assertion;
+  FIRAuthProtoFinalizeMFAPhoneRequestInfo *finalizeMFAPhoneRequestInfo =
+      [[FIRAuthProtoFinalizeMFAPhoneRequestInfo alloc] initWithSessionInfo:phoneAssertion.authCredential.verificationID
+                                                          verificationCode:phoneAssertion.authCredential.verificationCode];
+  FIRFinalizeMFAEnrollmentRequest *request =
+  [[FIRFinalizeMFAEnrollmentRequest alloc] initWithIDToken:self.user.rawAccessToken
+                                               MFAProvider:phoneAssertion.factorID
+                                               displayName:displayName
+                                          verificationInfo:finalizeMFAPhoneRequestInfo
+                                      requestConfiguration:self.user.requestConfiguration];
+  [FIRAuthBackend finalizeMultiFactorEnrollment:request
+                                       callback:^(FIRFinalizeMFAEnrollmentResponse * _Nullable response,
+                                                  NSError * _Nullable error) {
+                                         if (error) {
+                                           if (completion) {
+                                             completion(error);
+                                           }
+                                         } else {
+  [FIRAuth.auth completeSignInWithAccessToken:response.IDToken
+                   accessTokenExpirationDate:nil
+                                refreshToken:response.refreshToken
+                                   anonymous:NO
+                                    callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+      FIRAuthDataResult* result = [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil];
+      FIRAuthDataResultCallback decoratedCallback =
+          [FIRAuth.auth signInFlowAuthDataResultCallbackByDecoratingCallback:^(FIRAuthDataResult *_Nullable authResult,
+                                                                               NSError *_Nullable error) {
+            if (completion) {
+              completion(error);
+            }
+          }];
+      decoratedCallback(result, error);
+    }];
+   }
+ }];
+#endif
+}
+
+- (void)unenrollWithInfo:(FIRMultiFactorInfo *)factorInfo
+              completion:(nullable FIRAuthVoidErrorCallback)completion {
+  [self unenrollWithFactorUID:factorInfo.UID completion:completion];
+}
+
+- (void)unenrollWithFactorUID:(NSString *)factorUID
+                   completion:(nullable FIRAuthVoidErrorCallback)completion {
+  FIRWithdrawMFARequest *request = [[FIRWithdrawMFARequest alloc] initWithIDToken:self.user.rawAccessToken
+                                                                  MFAEnrollmentID:factorUID
+                                                             requestConfiguration:self.user.requestConfiguration];
+  [FIRAuthBackend withdrawMultiFactor:request
+                             callback:^(FIRWithdrawMFAResponse *_Nullable response, NSError * _Nullable error) {
+                               if (error) {
+                                 if (completion) {
+                                   completion(error);
+                                 }
+                               } else {
+  [FIRAuth.auth completeSignInWithAccessToken:response.IDToken
+                   accessTokenExpirationDate:nil
+                                refreshToken:response.refreshToken
+                                   anonymous:NO
+                                    callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+      FIRAuthDataResult* result = [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil];
+      FIRAuthDataResultCallback decoratedCallback =
+      [FIRAuth.auth signInFlowAuthDataResultCallbackByDecoratingCallback:^(FIRAuthDataResult *_Nullable authResult,
+                                                                           NSError *_Nullable error) {
+        if (error) {
+          [[FIRAuth auth] signOut:NULL];
+        }
+        if (completion) {
+          completion(error);
+        }
+      }];
+      decoratedCallback(result, error);
+    }];
+   }
+ }];
+}
+
+#pragma mark - Internal
+
+- (instancetype)initWithMFAEnrollments:(NSArray<FIRAuthProtoMFAEnrollment *> *)MFAEnrollments {
+  self = [super init];
+
+  if (self) {
+    NSMutableArray<FIRMultiFactorInfo *> *multiFactorInfoArray = [[NSMutableArray alloc] init];
+    for (FIRAuthProtoMFAEnrollment *MFAEnrollment in MFAEnrollments) {
+      FIRMultiFactorInfo *multiFactorInfo = [[FIRMultiFactorInfo alloc] initWithProto:MFAEnrollment];
+      [multiFactorInfoArray addObject:multiFactorInfo];
+    }
+    _enrolledFactors = [multiFactorInfoArray copy];
+  }
+
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+  self = [self init];
+  if (self) {
+    NSArray<FIRMultiFactorInfo *> *enrolledFactors
+        = [aDecoder decodeObjectOfClass:[NSArray<FIRMultiFactorInfo *> class]
+                                 forKey:kEnrolledFactorsCodingKey];
+    _enrolledFactors = enrolledFactors;
+    _user = [aDecoder decodeObjectOfClass:[FIRUser class] forKey:kUserCodingKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+  [aCoder encodeObject:_enrolledFactors forKey:kEnrolledFactorsCodingKey];
+  [aCoder encodeObject:_user forKey:kUserCodingKey];
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 34 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorAssertion+Internal.h

@@ -0,0 +1,34 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactorAssertion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRMultiFactorAssertion () {
+
+@protected
+  NSString *_factorID;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 29 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorAssertion.m

@@ -0,0 +1,29 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactorAssertion.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRMultiFactorAssertion
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 26 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorConstants.m

@@ -0,0 +1,26 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import <Foundation/Foundation.h>
+
+#pragma mark - Multi Factor ID constants
+
+NSString *const FIRPhoneMultiFactorID = @"1";
+
+#endif

+ 37 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorInfo+Internal.h

@@ -0,0 +1,37 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactorInfo.h"
+
+#import "FIRAuthProtoMFAEnrollment.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRMultiFactorInfo () {
+  @protected
+  NSString *_factorID;
+}
+
+- (instancetype)initWithProto:(FIRAuthProtoMFAEnrollment *)proto;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 74 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorInfo.m

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactorInfo.h"
+
+#import "FIRAuthProtoMFAEnrollment.h"
+
+static NSString *kUIDCodingKey = @"uid";
+
+static NSString *kDisplayNameCodingKey = @"displayName";
+
+static NSString *kEnrollmentDateCodingKey = @"enrollmentDate";
+
+static NSString *kFactorIDCodingKey = @"factorID";
+
+@implementation FIRMultiFactorInfo
+
+#pragma mark - Internal
+
+- (instancetype)initWithProto:(FIRAuthProtoMFAEnrollment *)proto {
+  self = [super init];
+
+  if (self) {
+    _UID = proto.MFAEnrollmentID;
+    _displayName = proto.displayName;
+    _enrollmentDate = proto.enrolledAt;
+  }
+
+  return self;
+}
+
+#pragma mark - NSSecureCoding
+
++ (BOOL)supportsSecureCoding {
+  return YES;
+}
+
+- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
+  self = [self init];
+  if (self) {
+    _UID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUIDCodingKey];
+    _displayName = [aDecoder decodeObjectOfClass:[NSString class] forKey:kDisplayNameCodingKey];
+    _enrollmentDate = [aDecoder decodeObjectOfClass:[NSDate class] forKey:kEnrollmentDateCodingKey];
+    _factorID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kFactorIDCodingKey];
+  }
+  return self;
+}
+
+- (void)encodeWithCoder:(NSCoder *)aCoder {
+  [aCoder encodeObject:_UID forKey:kUIDCodingKey];
+  [aCoder encodeObject:_displayName forKey:kDisplayNameCodingKey];
+  [aCoder encodeObject:_enrollmentDate forKey:kEnrollmentDateCodingKey];
+  [aCoder encodeObject:_factorID forKey:kFactorIDCodingKey];
+}
+
+@end
+
+#endif

+ 35 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorResolver+Internal.h

@@ -0,0 +1,35 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+
+#import "FIRMultiFactorResolver.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRMultiFactorResolver ()
+
+@property(nonatomic) NSString *MFAPendingCredential;
+
+- (instancetype)initWithMFAPendingCredential:(NSString *_Nullable)MFAPendingCredential
+                                       hints:(NSArray<FIRMultiFactorInfo *> *)hints;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 95 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorResolver.m

@@ -0,0 +1,95 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+
+#import "FIRMultiFactorResolver.h"
+
+#import "FIRAdditionalUserInfo.h"
+#import "FIRAuthBackend+MultiFactor.h"
+#import "FIRAuthDataResult_Internal.h"
+#import "FIRAuthProtoFinalizeMFAPhoneRequestInfo.h"
+#import "FIRAuth_Internal.h"
+#import "FIRFinalizeMFASignInRequest.h"
+#import "FIRMultiFactorResolver+Internal.h"
+#import "FIRMultiFactorSession+Internal.h"
+
+#if TARGET_OS_IOS
+#import "FIRPhoneAuthCredential_Internal.h"
+#import "FIRPhoneMultiFactorAssertion+Internal.h"
+#import "FIRPhoneMultiFactorAssertion.h"
+#endif
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRMultiFactorResolver
+
+- (instancetype)initWithMFAPendingCredential:(NSString *_Nullable)MFAPendingCredential
+                                       hints:(NSArray<FIRMultiFactorInfo *> *)hints {
+  self = [super init];
+  if (self) {
+    _MFAPendingCredential = MFAPendingCredential;
+    _hints = hints;
+    _auth = [FIRAuth auth];
+    _session = [[FIRMultiFactorSession alloc] init];
+    _session.MFAPendingCredential = MFAPendingCredential;
+  }
+  return self;
+}
+
+- (void)resolveSignInWithAssertion:(nonnull FIRMultiFactorAssertion *)assertion
+                        completion:(nullable FIRAuthDataResultCallback)completion {
+#if TARGET_OS_IOS
+  FIRPhoneMultiFactorAssertion *phoneAssertion = (FIRPhoneMultiFactorAssertion *)assertion;
+  FIRAuthProtoFinalizeMFAPhoneRequestInfo *finalizeMFAPhoneRequestInfo =
+      [[FIRAuthProtoFinalizeMFAPhoneRequestInfo alloc]
+       initWithSessionInfo:phoneAssertion.authCredential.verificationID
+       verificationCode:phoneAssertion.authCredential.verificationCode];
+  FIRFinalizeMFASignInRequest *request =
+  [[FIRFinalizeMFASignInRequest alloc] initWithMFAProvider:phoneAssertion.factorID
+                                      MFAPendingCredential:self.MFAPendingCredential
+                                          verificationInfo:finalizeMFAPhoneRequestInfo
+                                      requestConfiguration:self.auth.requestConfiguration];
+  [FIRAuthBackend finalizeMultiFactorSignIn:request
+                                   callback:^(FIRFinalizeMFASignInResponse * _Nullable response,
+                                              NSError * _Nullable error) {
+   if (error) {
+     if (completion) {
+       completion(nil, error);
+     }
+   } else {
+     [FIRAuth.auth completeSignInWithAccessToken:response.IDToken
+                       accessTokenExpirationDate:nil
+                            refreshToken:response.refreshToken
+                               anonymous:NO
+                                callback:^(FIRUser *_Nullable user, NSError *_Nullable error) {
+      FIRAuthDataResult* result =
+          [[FIRAuthDataResult alloc] initWithUser:user additionalUserInfo:nil];
+      FIRAuthDataResultCallback decoratedCallback =
+          [FIRAuth.auth signInFlowAuthDataResultCallbackByDecoratingCallback:completion];
+      decoratedCallback(result, error);
+    }];
+   }
+ }];
+#endif
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 40 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorSession+Internal.h

@@ -0,0 +1,40 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+#import "FIRMultiFactorSession.h"
+
+#import "FIRMultiFactorInfo.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FIRMultiFactorSession ()
+
+@property(nonatomic, readonly) NSString *IDToken;
+
+@property(nonatomic) NSString *MFAPendingCredential;
+
+@property(nonatomic) FIRMultiFactorInfo *multiFactorInfo;
+
++ (FIRMultiFactorSession *)sessionForCurrentUser;
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

+ 53 - 0
Firebase/Auth/Source/MultiFactor/FIRMultiFactorSession.m

@@ -0,0 +1,53 @@
+/*
+ * Copyright 2019 Google
+ *
+ * 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/LICENSE2.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.
+ */
+#include <TargetConditionals.h>
+#if TARGET_OS_IOS
+
+
+#import "FIRMultiFactorSession.h"
+#import "FIRMultiFactorSession+Internal.h"
+
+#import "FIRAuth.h"
+#import "FIRUser_Internal.h"
+
+NS_ASSUME_NONNULL_BEGIN
+
+@implementation FIRMultiFactorSession
+
+#pragma mark - Private
+
+- (instancetype)initWithIDToken:(NSString *)IDToken {
+  self = [super init];
+  if (self) {
+    _IDToken = IDToken;
+  }
+  return self;
+}
+
+#pragma mark - Internal
+
++ (FIRMultiFactorSession *)sessionForCurrentUser {
+  FIRUser *currentUser = [[FIRAuth auth] currentUser];
+  NSString *IDToken = currentUser.rawAccessToken;
+  FIRMultiFactorSession *session = [[FIRMultiFactorSession alloc] initWithIDToken:IDToken];
+  return session;
+}
+
+@end
+
+NS_ASSUME_NONNULL_END
+
+#endif

部分文件因文件數量過多而無法顯示