Forráskód Böngészése

[Auth] TOTP support for macOS (#15112)

Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com>
Ben Hagen 7 hónapja
szülő
commit
b7db7b4492
26 módosított fájl, 135 hozzáadás és 94 törlés
  1. 3 0
      FirebaseAuth/CHANGELOG.md
  2. 1 1
      FirebaseAuth/README.md
  3. 1 1
      FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m
  4. 7 8
      FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h
  5. 4 4
      FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift
  6. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift
  7. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift
  8. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift
  9. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift
  10. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift
  11. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift
  12. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift
  13. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift
  14. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift
  15. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift
  16. 2 2
      FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift
  17. 31 18
      FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift
  18. 7 7
      FirebaseAuth/Sources/Swift/User/User.swift
  19. 2 2
      FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift
  20. 4 2
      FirebaseAuth/Tests/Unit/ObjCAPITests.m
  21. 3 1
      FirebaseAuth/Tests/Unit/ObjCGlobalTests.m
  22. 3 1
      FirebaseAuth/Tests/Unit/SwiftAPI.swift
  23. 3 2
      FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift
  24. 38 17
      FirebaseAuth/Tests/Unit/UserTests.swift
  25. 3 4
      FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift
  26. 3 4
      FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift

+ 3 - 0
FirebaseAuth/CHANGELOG.md

@@ -1,3 +1,6 @@
+# Unreleased
+- [added] Added TOTP support for macOS.
+
 # 12.1.0
 - [fixed] Fix a formatting issue with generated TOTP URLs that prevented them
   from working with the Google Authenticator app. (#15128)

+ 1 - 1
FirebaseAuth/README.md

@@ -1,4 +1,4 @@
-# Firebase Auth for iOS
+# Firebase Auth for iOS and macOS
 
 Firebase Auth enables apps to easily support multiple authentication options
 for their end users.

+ 1 - 1
FirebaseAuth/Sources/ObjC/FIRMultiFactorConstants.m

@@ -15,7 +15,7 @@
  */
 
 #import <TargetConditionals.h>
-#if TARGET_OS_IOS
+#if TARGET_OS_IOS || TARGET_OS_OSX
 
 #import <Foundation/Foundation.h>
 

+ 7 - 8
FirebaseAuth/Sources/Public/FirebaseAuth/FIRMultiFactor.h

@@ -5,7 +5,7 @@
  * 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
+ *      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,
@@ -22,27 +22,26 @@ NS_ASSUME_NONNULL_BEGIN
 
 /** @typedef FIRMultiFactorSessionCallback
     @brief The callback that triggered when a developer calls `getSessionWithCompletion`.
-        This type is available on iOS only.
+        This type is available on iOS and macOS.
     @param session The multi factor session returned, if any.
     @param error The error which occurred, if any.
 */
 typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session,
                                               NSError *_Nullable error)
-    NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.")
-        API_UNAVAILABLE(macos, tvos, watchos);
+    NS_SWIFT_UNAVAILABLE("Use Swift's closure syntax instead.") API_UNAVAILABLE(tvos, watchos);
 
 /**
    @brief The string identifier for using phone as a second factor.
-        This constant is available on iOS only.
+        This constant is available on iOS and macOS.
 */
 extern NSString *const _Nonnull FIRPhoneMultiFactorID NS_SWIFT_NAME(PhoneMultiFactorID)
-    API_UNAVAILABLE(macos, tvos, watchos);
+    API_UNAVAILABLE(tvos, watchos);
 
 /**
    @brief The string identifier for using TOTP as a second factor.
-        This constant is available on iOS only.
+        This constant is available on iOS and macOS.
 */
 extern NSString *const _Nonnull FIRTOTPMultiFactorID NS_SWIFT_NAME(TOTPMultiFactorID)
-    API_UNAVAILABLE(macos, tvos, watchos);
+    API_UNAVAILABLE(tvos, watchos);
 
 NS_ASSUME_NONNULL_END

+ 4 - 4
FirebaseAuth/Sources/Swift/Backend/AuthBackend.swift

@@ -99,9 +99,7 @@ final class AuthBackend: AuthBackendProtocol {
   }
 
   private static func generateMFAError(response: AuthRPCResponse, auth: Auth) -> Error? {
-    #if !os(iOS)
-      return nil
-    #else
+    #if os(iOS) || os(macOS)
       if let mfaResponse = response as? AuthMFAResponse,
          mfaResponse.idToken == nil,
          let enrollments = mfaResponse.mfaInfo {
@@ -124,7 +122,9 @@ final class AuthBackend: AuthBackendProtocol {
       } else {
         return nil
       }
-    #endif // !os(iOS)
+    #else
+      return nil
+    #endif // os(iOS) || os(macOS)
   }
 
   // Check whether or not the successful response is actually the special case phone

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactor.swift

@@ -14,7 +14,7 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   extension MultiFactor: NSSecureCoding {}
@@ -22,7 +22,7 @@ import Foundation
   /// The interface defining the multi factor related properties and operations pertaining to a
   /// user.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRMultiFactor) open class MultiFactor: NSObject {
     @objc open var enrolledFactors: [MultiFactorInfo]

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorAssertion.swift

@@ -14,12 +14,12 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// The base class for asserting ownership of a second factor. This is equivalent to the
   ///    AuthCredential class.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject {
     /// The second factor identifier for this opaque object asserting a second factor.
     @objc open var factorID: String

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorInfo.swift

@@ -16,12 +16,12 @@ import Foundation
 
 // TODO(Swift 6 Breaking): Make checked Sendable.
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
   extension MultiFactorInfo: NSSecureCoding {}
 
   /// Safe public structure used to represent a second factor entity from a client perspective.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRMultiFactorInfo) open class MultiFactorInfo: NSObject, @unchecked Sendable {
     /// The multi-factor enrollment ID.
     @objc(UID) public let uid: String

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorResolver.swift

@@ -14,12 +14,12 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// The subclass of base class `MultiFactorAssertion`, used to assert ownership of a phone
   /// second factor.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRMultiFactorResolver)
   open class MultiFactorResolver: NSObject {

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/MultiFactorSession.swift

@@ -14,7 +14,7 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// Opaque object that identifies the current session to enroll a second factor or to
   /// complete sign in when previously enrolled.
@@ -23,7 +23,7 @@ import Foundation
   /// or to complete sign in when previously enrolled. It contains additional context on the
   /// existing user, notably the confirmation that the user passed the first factor challenge.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject {
     /// The ID token for an enroll flow. This has to be retrieved after recent authentication.

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorAssertion.swift

@@ -14,12 +14,12 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone
   /// second factor.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRPhoneMultiFactorAssertion) open class PhoneMultiFactorAssertion: MultiFactorAssertion {
     var authCredential: PhoneAuthCredential?

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorGenerator.swift

@@ -14,14 +14,14 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// The data structure used to help initialize an assertion for a second factor entity to the
   /// Firebase Auth/CICP server.
   ///
   /// Depending on the type of second factor, this will help generate the assertion.
   ///
-  ///  This class is available on iOS only.
+  ///  This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRPhoneMultiFactorGenerator)
   open class PhoneMultiFactorGenerator: NSObject {

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/Phone/PhoneMultiFactorInfo.swift

@@ -16,13 +16,13 @@ import Foundation
 
 // TODO(Swift 6 Breaking): Make checked Sendable.
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// Extends the MultiFactorInfo class for phone number second factors.
   ///
   /// The identifier of this second factor is "phone".
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRPhoneMultiFactorInfo) open class PhoneMultiFactorInfo: MultiFactorInfo,
     @unchecked Sendable {
     /// The string identifier for using phone as a second factor.

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultFactorAssertion.swift

@@ -14,7 +14,7 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   enum SecretOrID {
     case secret(TOTPSecret)
@@ -24,7 +24,7 @@ import Foundation
   /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP
   /// (Time-based One Time Password) second factor.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRTOTPMultiFactorAssertion) open class TOTPMultiFactorAssertion: MultiFactorAssertion {
     let oneTimePassword: String
     let secretOrID: SecretOrID

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorGenerator.swift

@@ -14,13 +14,13 @@
 
 import Foundation
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// The data structure used to help initialize an assertion for a second factor entity to the
   /// Firebase Auth/CICP server. Depending on the type of second factor, this will help generate
   /// the assertion.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject {
     /// Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a

+ 2 - 2
FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPMultiFactorInfo.swift

@@ -17,13 +17,13 @@ import Foundation
 // TODO(Swift 6 Breaking): Make checked Sendable. Also, does this need
 // to be public?
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
   /// Extends the MultiFactorInfo class for time based one-time password second factors.
   ///
   /// The identifier of this second factor is "totp".
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   class TOTPMultiFactorInfo: MultiFactorInfo, @unchecked Sendable {
     /// Initialize the AuthProtoMFAEnrollment instance with proto.
     /// - Parameter proto: AuthProtoMFAEnrollment proto object.

+ 31 - 18
FirebaseAuth/Sources/Swift/MultiFactor/TOTP/TOTPSecret.swift

@@ -19,13 +19,17 @@ import Foundation
   internal import GoogleUtilities_Environment
 #endif
 
-#if os(iOS)
-  import UIKit
+#if os(iOS) || os(macOS)
+  #if os(iOS)
+    import UIKit
+  #elseif os(macOS)
+    import AppKit
+  #endif
 
   /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP
   /// (Time-based One Time Password) second factor.
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRTOTPSecret) open class TOTPSecret: NSObject {
     /// Returns the shared secret key/seed used to generate time-based one-time passwords.
     @objc open func sharedSecretKey() -> String {
@@ -57,24 +61,33 @@ import Foundation
     @MainActor @objc(openInOTPAppWithQRCodeURL:)
     open func openInOTPApp(withQRCodeURL qrCodeURL: String) {
       if GULAppEnvironmentUtil.isAppExtension() {
-        // iOS App extensions should not call [UIApplication sharedApplication], even if
-        // UIApplication responds to it.
+        // App extensions should not call [UIApplication sharedApplication] or [NSWorkspace
+        // sharedWorkspace], even if they respond to it.
         return
       }
 
-      // Using reflection here to avoid build errors in extensions.
-      let sel = NSSelectorFromString("sharedApplication")
-      guard UIApplication.responds(to: sel),
-            let rawApplication = UIApplication.perform(sel),
-            let application = rawApplication.takeUnretainedValue() as? UIApplication else {
-        return
-      }
-      if let url = URL(string: qrCodeURL), application.canOpenURL(url) {
-        application.open(url, options: [:], completionHandler: nil)
-      } else {
-        AuthLog.logError(code: "I-AUT000019",
-                         message: "URL: \(qrCodeURL) cannot be opened")
-      }
+      #if os(iOS)
+        // Using reflection here to avoid build errors in extensions.
+        let sel = NSSelectorFromString("sharedApplication")
+        guard UIApplication.responds(to: sel),
+              let rawApplication = UIApplication.perform(sel),
+              let application = rawApplication.takeUnretainedValue() as? UIApplication else {
+          return
+        }
+        if let url = URL(string: qrCodeURL), application.canOpenURL(url) {
+          application.open(url, options: [:], completionHandler: nil)
+        } else {
+          AuthLog.logError(code: "I-AUT000019",
+                           message: "URL: \(qrCodeURL) cannot be opened")
+        }
+      #elseif os(macOS)
+        if let url = URL(string: qrCodeURL) {
+          NSWorkspace.shared.open(url)
+        } else {
+          AuthLog.logError(code: "I-AUT000019",
+                           message: "URL: \(qrCodeURL) cannot be opened")
+        }
+      #endif
     }
 
     /// Shared secret key/seed used for enrolling in TOTP MFA and generating OTPs.

+ 7 - 7
FirebaseAuth/Sources/Swift/User/User.swift

@@ -58,10 +58,10 @@ extension User: NSSecureCoding {}
   /// The tenant ID of the current user. `nil` if none is available.
   @objc public private(set) var tenantID: String?
 
-  #if os(iOS)
+  #if os(iOS) || os(macOS)
     /// Multi factor object associated with the user.
     ///
-    /// This property is available on iOS only.
+    /// This property is available on iOS and macOS.
     @objc public private(set) var multiFactor: MultiFactor
   #endif
 
@@ -1066,7 +1066,7 @@ extension User: NSSecureCoding {}
     isEmailVerified = false
     metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil)
     tenantID = nil
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       multiFactor = MultiFactor(withMFAEnrollments: [])
     #endif
     uid = ""
@@ -1297,7 +1297,7 @@ extension User: NSSecureCoding {}
       }
     }
     providerDataRaw = providerData
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       if let enrollments = user.mfaEnrollments {
         multiFactor = MultiFactor(withMFAEnrollments: enrollments)
       }
@@ -1718,7 +1718,7 @@ extension User: NSSecureCoding {}
       coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey)
     }
     coder.encode(tokenService, forKey: kTokenServiceCodingKey)
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       coder.encode(multiFactor, forKey: kMultiFactorCodingKey)
     #endif
   }
@@ -1747,7 +1747,7 @@ extension User: NSSecureCoding {}
       as? [String: UserInfoImpl]
     let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey)
     let tenantID = coder.decodeObject(of: NSString.self, forKey: kTenantIDCodingKey) as? String
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       let multiFactor = coder.decodeObject(of: MultiFactor.self, forKey: kMultiFactorCodingKey)
     #endif
     self.tokenService = tokenService
@@ -1778,7 +1778,7 @@ extension User: NSSecureCoding {}
     backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())
 
     userProfileUpdate = UserProfileUpdate()
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       self.multiFactor = multiFactor ?? MultiFactor()
       super.init()
       multiFactor?.user = self

+ 2 - 2
FirebaseAuth/Sources/Swift/Utilities/AuthErrorUtils.swift

@@ -568,7 +568,7 @@ class AuthErrorUtils {
     return error(code: .blockingCloudFunctionError, message: errorMessage)
   }
 
-  #if os(iOS)
+  #if os(iOS) || os(macOS)
     static func secondFactorRequiredError(pendingCredential: String?,
                                           hints: [MultiFactorInfo],
                                           auth: Auth)
@@ -581,7 +581,7 @@ class AuthErrorUtils {
 
       return error(code: .secondFactorRequired, userInfo: userInfo)
     }
-  #endif // os(iOS)
+  #endif // os(iOS) || os(macOS)
 
   static func recaptchaSDKNotLinkedError() -> Error {
     // TODO(ObjC): point the link to GCIP doc once available.

+ 4 - 2
FirebaseAuth/Tests/Unit/ObjCAPITests.m

@@ -372,7 +372,7 @@
                                                                    accessToken:@"token"];
 }
 
-#if TARGET_OS_IOS
+#if TARGET_OS_IOS || TARGET_OS_OSX
 - (void)FIRMultiFactor_h:(FIRMultiFactor *)mf mfa:(FIRMultiFactorAssertion *)mfa {
   [mf getSessionWithCompletion:^(FIRMultiFactorSession *_Nullable credential,
                                  NSError *_Nullable error){
@@ -466,7 +466,9 @@
 - (void)phoneMultiFactorInfo:(FIRPhoneMultiFactorInfo *)info {
   __unused NSString *s = [info phoneNumber];
 }
+#endif
 
+#if TARGET_OS_IOS || TARGET_OS_OSX
 - (void)FIRTOTPSecret_h:(FIRTOTPSecret *)secret {
   NSString *s = [secret sharedSecretKey];
   s = [secret generateQRCodeURLWithAccountName:@"name" issuer:@"issuer"];
@@ -571,7 +573,7 @@
   b = [user isEmailVerified];
   __unused NSArray<NSObject<FIRUserInfo> *> *userInfo = [user providerData];
   __unused FIRUserMetadata *meta = [user metadata];
-#if TARGET_OS_IOS
+#if TARGET_OS_IOS || TARGET_OS_OSX
   __unused FIRMultiFactor *mf = [user multiFactor];
 #endif
   NSString *s = [user refreshToken];

+ 3 - 1
FirebaseAuth/Tests/Unit/ObjCGlobalTests.m

@@ -41,9 +41,11 @@
   s = FIRGoogleAuthSignInMethod;
 #if TARGET_OS_IOS
   s = FIRPhoneMultiFactorID;
-  s = FIRTOTPMultiFactorID;
   s = FIRPhoneAuthProviderID;
   s = FIRPhoneAuthSignInMethod;
+#endif
+#if TARGET_OS_IOS || TARGET_OS_OSX
+  s = FIRTOTPMultiFactorID;
 #endif
   s = FIRTwitterAuthProviderID;
   s = FIRTwitterAuthSignInMethod;

+ 3 - 1
FirebaseAuth/Tests/Unit/SwiftAPI.swift

@@ -548,7 +548,9 @@ class AuthAPI_hOnlyTests: XCTestCase {
     func phoneMultiFactorInfo(mfi: PhoneMultiFactorInfo) {
       let _: String = mfi.phoneNumber
     }
+  #endif
 
+  #if os(iOS) || os(macOS)
     func FIRTOTPSecret_h(session: MultiFactorSession) async throws {
       let obj = try await TOTPMultiFactorGenerator.generateSecret(with: session)
       _ = obj.sharedSecretKey()
@@ -638,7 +640,7 @@ class AuthAPI_hOnlyTests: XCTestCase {
     let _: Bool = user.isEmailVerified
     let _: [UserInfo] = user.providerData
     let _: UserMetadata = user.metadata
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       let _: MultiFactor = user.multiFactor
     #endif
     if let _: String = user.refreshToken,

+ 3 - 2
FirebaseAuth/Tests/Unit/SwiftGlobalTests.swift

@@ -38,9 +38,10 @@ class SwiftGlobalTests: XCTestCase {
     let _: String = GitHubAuthSignInMethod
     let _: String = GoogleAuthProviderID
     let _: String = GoogleAuthSignInMethod
-    #if os(iOS)
-      let _: String = PhoneMultiFactorID
+    #if os(iOS) || os(macOS)
       let _: String = TOTPMultiFactorID
+    #endif
+    #if os(iOS)
       let _: String = PhoneAuthProviderID
       let _: String = PhoneAuthSignInMethod
     #endif

+ 38 - 17
FirebaseAuth/Tests/Unit/UserTests.swift

@@ -135,6 +135,27 @@ class UserTests: RPCBaseTests {
       ])
     #endif
 
+    var mfaInfo: [[AnyHashable: AnyHashable]] = []
+
+    #if os(iOS)
+      mfaInfo.append([
+        "phoneInfo": kPhoneInfo,
+        "mfaEnrollmentId": kEnrollmentID,
+        "displayName": kDisplayName,
+        "enrolledAt": kEnrolledAt,
+      ])
+    #endif
+
+    #if os(iOS) || os(macOS)
+      mfaInfo.append([
+        // In practice, this will be an empty dictionary.
+        "totpInfo": [AnyHashable: AnyHashable](),
+        "mfaEnrollmentId": kEnrollmentID,
+        "displayName": kDisplayName,
+        "enrolledAt": kEnrolledAt,
+      ])
+    #endif
+
     rpcIssuer?.fakeGetAccountProviderJSON = [[
       kProviderUserInfoKey: providerUserInfos,
       kLocalIDKey: kLocalID,
@@ -146,21 +167,7 @@ class UserTests: RPCBaseTests {
       "phoneNumber": kPhoneNumber,
       "createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds
       "lastLoginAt": String(Int(kLastSignInDateTimeIntervalInSeconds) * 1000),
-      "mfaInfo": [
-        [
-          "phoneInfo": kPhoneInfo,
-          "mfaEnrollmentId": kEnrollmentID,
-          "displayName": kDisplayName,
-          "enrolledAt": kEnrolledAt,
-        ],
-        [
-          // In practice, this will be an empty dictionary.
-          "totpInfo": [AnyHashable: AnyHashable](),
-          "mfaEnrollmentId": kEnrollmentID,
-          "displayName": kDisplayName,
-          "enrolledAt": kEnrolledAt,
-        ] as [AnyHashable: AnyHashable],
-      ],
+      "mfaInfo": mfaInfo,
     ]]
 
     let expectation = self.expectation(description: #function)
@@ -247,9 +254,12 @@ class UserTests: RPCBaseTests {
         var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self,
                               UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self,
                               NSArray.self]
-        #if os(iOS)
+        #if os(iOS) || os(macOS)
           encodedClasses.append(MultiFactor.self)
-          encodedClasses.append(PhoneMultiFactorInfo.self)
+          encodedClasses.append(TOTPMultiFactorInfo.self)
+          #if os(iOS)
+            encodedClasses.append(PhoneMultiFactorInfo.self)
+          #endif
         #endif
 
         let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
@@ -370,6 +380,17 @@ class UserTests: RPCBaseTests {
             XCTAssertEqual("\(date)", kEnrolledAtMatch)
           }
         #endif
+
+        #if os(macOS)
+          // Verify TOTP MultiFactorInfo properties.
+          let enrolledFactors = try XCTUnwrap(user.multiFactor.enrolledFactors)
+          XCTAssertEqual(enrolledFactors.count, 1)
+          XCTAssertEqual(enrolledFactors[0].factorID, PhoneMultiFactorInfo.TOTPMultiFactorID)
+          XCTAssertEqual(enrolledFactors[0].uid, kEnrollmentID)
+          XCTAssertEqual(enrolledFactors[0].displayName, self.kDisplayName)
+          let date = try XCTUnwrap(enrolledFactors[0].enrollmentDate)
+          XCTAssertEqual("\(date)", kEnrolledAtMatch)
+        #endif
       } catch {
         XCTFail("Caught an error in \(#function): \(error)")
       }

+ 3 - 4
FirebaseCombineSwift/Sources/Auth/MultiFactor+Combine.swift

@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#if os(iOS) || targetEnvironment(macCatalyst)
+#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS)
 
   import Combine
   import FirebaseAuth
 
-  @available(iOS 13.0, macCatalyst 13.0, *)
-  @available(macOS, unavailable)
+  @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *)
   @available(tvOS, unavailable)
   @available(watchOS, unavailable)
   public extension MultiFactor {
@@ -111,4 +110,4 @@
     }
   }
 
-#endif // os(iOS) || targetEnvironment(macCatalyst)
+#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS)

+ 3 - 4
FirebaseCombineSwift/Sources/Auth/MultiFactorResolver+Combine.swift

@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#if os(iOS) || targetEnvironment(macCatalyst)
+#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS)
 
   import Combine
   import FirebaseAuth
 
-  @available(iOS 13.0, macCatalyst 13.0, *)
-  @available(macOS, unavailable)
+  @available(iOS 13.0, macCatalyst 13.0, macOS 10.15, *)
   @available(tvOS, unavailable)
   @available(watchOS, unavailable)
   public extension MultiFactorResolver {
@@ -43,4 +42,4 @@
     }
   }
 
-#endif // os(iOS) || targetEnvironment(macCatalyst)
+#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS)