Procházet zdrojové kódy

[Auth] TOTP support for macOS (#15112)

Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com>
Ben Hagen před 7 měsíci
rodič
revize
b7db7b4492
26 změnil soubory, kde provedl 135 přidání a 94 odebrání
  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
 # 12.1.0
 - [fixed] Fix a formatting issue with generated TOTP URLs that prevented them
 - [fixed] Fix a formatting issue with generated TOTP URLs that prevented them
   from working with the Google Authenticator app. (#15128)
   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
 Firebase Auth enables apps to easily support multiple authentication options
 for their end users.
 for their end users.

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

@@ -15,7 +15,7 @@
  */
  */
 
 
 #import <TargetConditionals.h>
 #import <TargetConditionals.h>
-#if TARGET_OS_IOS
+#if TARGET_OS_IOS || TARGET_OS_OSX
 
 
 #import <Foundation/Foundation.h>
 #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 not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  * 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
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * distributed under the License is distributed on an "AS IS" BASIS,
@@ -22,27 +22,26 @@ NS_ASSUME_NONNULL_BEGIN
 
 
 /** @typedef FIRMultiFactorSessionCallback
 /** @typedef FIRMultiFactorSessionCallback
     @brief The callback that triggered when a developer calls `getSessionWithCompletion`.
     @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 session The multi factor session returned, if any.
     @param error The error which occurred, if any.
     @param error The error which occurred, if any.
 */
 */
 typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session,
 typedef void (^FIRMultiFactorSessionCallback)(FIRMultiFactorSession *_Nullable session,
                                               NSError *_Nullable error)
                                               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.
    @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)
 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.
    @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)
 extern NSString *const _Nonnull FIRTOTPMultiFactorID NS_SWIFT_NAME(TOTPMultiFactorID)
-    API_UNAVAILABLE(macos, tvos, watchos);
+    API_UNAVAILABLE(tvos, watchos);
 
 
 NS_ASSUME_NONNULL_END
 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? {
   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,
       if let mfaResponse = response as? AuthMFAResponse,
          mfaResponse.idToken == nil,
          mfaResponse.idToken == nil,
          let enrollments = mfaResponse.mfaInfo {
          let enrollments = mfaResponse.mfaInfo {
@@ -124,7 +122,9 @@ final class AuthBackend: AuthBackendProtocol {
       } else {
       } else {
         return nil
         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
   // 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
 import Foundation
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   extension MultiFactor: NSSecureCoding {}
   extension MultiFactor: NSSecureCoding {}
@@ -22,7 +22,7 @@ import Foundation
   /// The interface defining the multi factor related properties and operations pertaining to a
   /// The interface defining the multi factor related properties and operations pertaining to a
   /// user.
   /// 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, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRMultiFactor) open class MultiFactor: NSObject {
   @objc(FIRMultiFactor) open class MultiFactor: NSObject {
     @objc open var enrolledFactors: [MultiFactorInfo]
     @objc open var enrolledFactors: [MultiFactorInfo]

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

@@ -14,12 +14,12 @@
 
 
 import Foundation
 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
   /// The base class for asserting ownership of a second factor. This is equivalent to the
   ///    AuthCredential class.
   ///    AuthCredential class.
   ///
   ///
-  /// This class is available on iOS only.
+  /// This class is available on iOS and macOS.
   @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject {
   @objc(FIRMultiFactorAssertion) open class MultiFactorAssertion: NSObject {
     /// The second factor identifier for this opaque object asserting a second factor.
     /// The second factor identifier for this opaque object asserting a second factor.
     @objc open var factorID: String
     @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.
 // TODO(Swift 6 Breaking): Make checked Sendable.
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
   extension MultiFactorInfo: NSSecureCoding {}
   extension MultiFactorInfo: NSSecureCoding {}
 
 
   /// Safe public structure used to represent a second factor entity from a client perspective.
   /// 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 {
   @objc(FIRMultiFactorInfo) open class MultiFactorInfo: NSObject, @unchecked Sendable {
     /// The multi-factor enrollment ID.
     /// The multi-factor enrollment ID.
     @objc(UID) public let uid: String
     @objc(UID) public let uid: String

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

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

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

@@ -14,7 +14,7 @@
 
 
 import Foundation
 import Foundation
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   /// Opaque object that identifies the current session to enroll a second factor or to
   /// Opaque object that identifies the current session to enroll a second factor or to
   /// complete sign in when previously enrolled.
   /// 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
   /// 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.
   /// 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, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject {
   @objc(FIRMultiFactorSession) open class MultiFactorSession: NSObject {
     /// The ID token for an enroll flow. This has to be retrieved after recent authentication.
     /// 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
 import Foundation
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone
   /// The subclass of base class FIRMultiFactorAssertion, used to assert ownership of a phone
   /// second factor.
   /// 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, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRPhoneMultiFactorAssertion) open class PhoneMultiFactorAssertion: MultiFactorAssertion {
   @objc(FIRPhoneMultiFactorAssertion) open class PhoneMultiFactorAssertion: MultiFactorAssertion {
     var authCredential: PhoneAuthCredential?
     var authCredential: PhoneAuthCredential?

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

@@ -14,14 +14,14 @@
 
 
 import Foundation
 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
   /// The data structure used to help initialize an assertion for a second factor entity to the
   /// Firebase Auth/CICP server.
   /// Firebase Auth/CICP server.
   ///
   ///
   /// Depending on the type of second factor, this will help generate the assertion.
   /// 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, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRPhoneMultiFactorGenerator)
   @objc(FIRPhoneMultiFactorGenerator)
   open class PhoneMultiFactorGenerator: NSObject {
   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.
 // TODO(Swift 6 Breaking): Make checked Sendable.
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   /// Extends the MultiFactorInfo class for phone number second factors.
   /// Extends the MultiFactorInfo class for phone number second factors.
   ///
   ///
   /// The identifier of this second factor is "phone".
   /// 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,
   @objc(FIRPhoneMultiFactorInfo) open class PhoneMultiFactorInfo: MultiFactorInfo,
     @unchecked Sendable {
     @unchecked Sendable {
     /// The string identifier for using phone as a second factor.
     /// 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
 import Foundation
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   enum SecretOrID {
   enum SecretOrID {
     case secret(TOTPSecret)
     case secret(TOTPSecret)
@@ -24,7 +24,7 @@ import Foundation
   /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP
   /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP
   /// (Time-based One Time Password) second factor.
   /// (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 {
   @objc(FIRTOTPMultiFactorAssertion) open class TOTPMultiFactorAssertion: MultiFactorAssertion {
     let oneTimePassword: String
     let oneTimePassword: String
     let secretOrID: SecretOrID
     let secretOrID: SecretOrID

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

@@ -14,13 +14,13 @@
 
 
 import Foundation
 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
   /// 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
   /// Firebase Auth/CICP server. Depending on the type of second factor, this will help generate
   /// the assertion.
   /// 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, *)
   @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
   @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject {
   @objc(FIRTOTPMultiFactorGenerator) open class TOTPMultiFactorGenerator: NSObject {
     /// Creates a TOTP secret as part of enrolling a TOTP second factor. Used for generating a
     /// 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
 // TODO(Swift 6 Breaking): Make checked Sendable. Also, does this need
 // to be public?
 // to be public?
 
 
-#if os(iOS)
+#if os(iOS) || os(macOS)
 
 
   /// Extends the MultiFactorInfo class for time based one-time password second factors.
   /// Extends the MultiFactorInfo class for time based one-time password second factors.
   ///
   ///
   /// The identifier of this second factor is "totp".
   /// 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 {
   class TOTPMultiFactorInfo: MultiFactorInfo, @unchecked Sendable {
     /// Initialize the AuthProtoMFAEnrollment instance with proto.
     /// Initialize the AuthProtoMFAEnrollment instance with proto.
     /// - Parameter proto: AuthProtoMFAEnrollment proto object.
     /// - Parameter proto: AuthProtoMFAEnrollment proto object.

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

@@ -19,13 +19,17 @@ import Foundation
   internal import GoogleUtilities_Environment
   internal import GoogleUtilities_Environment
 #endif
 #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
   /// The subclass of base class MultiFactorAssertion, used to assert ownership of a TOTP
   /// (Time-based One Time Password) second factor.
   /// (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 {
   @objc(FIRTOTPSecret) open class TOTPSecret: NSObject {
     /// Returns the shared secret key/seed used to generate time-based one-time passwords.
     /// Returns the shared secret key/seed used to generate time-based one-time passwords.
     @objc open func sharedSecretKey() -> String {
     @objc open func sharedSecretKey() -> String {
@@ -57,24 +61,33 @@ import Foundation
     @MainActor @objc(openInOTPAppWithQRCodeURL:)
     @MainActor @objc(openInOTPAppWithQRCodeURL:)
     open func openInOTPApp(withQRCodeURL qrCodeURL: String) {
     open func openInOTPApp(withQRCodeURL qrCodeURL: String) {
       if GULAppEnvironmentUtil.isAppExtension() {
       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
         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.
     /// 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.
   /// The tenant ID of the current user. `nil` if none is available.
   @objc public private(set) var tenantID: String?
   @objc public private(set) var tenantID: String?
 
 
-  #if os(iOS)
+  #if os(iOS) || os(macOS)
     /// Multi factor object associated with the user.
     /// 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
     @objc public private(set) var multiFactor: MultiFactor
   #endif
   #endif
 
 
@@ -1066,7 +1066,7 @@ extension User: NSSecureCoding {}
     isEmailVerified = false
     isEmailVerified = false
     metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil)
     metadata = UserMetadata(withCreationDate: nil, lastSignInDate: nil)
     tenantID = nil
     tenantID = nil
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       multiFactor = MultiFactor(withMFAEnrollments: [])
       multiFactor = MultiFactor(withMFAEnrollments: [])
     #endif
     #endif
     uid = ""
     uid = ""
@@ -1297,7 +1297,7 @@ extension User: NSSecureCoding {}
       }
       }
     }
     }
     providerDataRaw = providerData
     providerDataRaw = providerData
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       if let enrollments = user.mfaEnrollments {
       if let enrollments = user.mfaEnrollments {
         multiFactor = MultiFactor(withMFAEnrollments: enrollments)
         multiFactor = MultiFactor(withMFAEnrollments: enrollments)
       }
       }
@@ -1718,7 +1718,7 @@ extension User: NSSecureCoding {}
       coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey)
       coder.encode(auth.requestConfiguration.appID, forKey: kFirebaseAppIDCodingKey)
     }
     }
     coder.encode(tokenService, forKey: kTokenServiceCodingKey)
     coder.encode(tokenService, forKey: kTokenServiceCodingKey)
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       coder.encode(multiFactor, forKey: kMultiFactorCodingKey)
       coder.encode(multiFactor, forKey: kMultiFactorCodingKey)
     #endif
     #endif
   }
   }
@@ -1747,7 +1747,7 @@ extension User: NSSecureCoding {}
       as? [String: UserInfoImpl]
       as? [String: UserInfoImpl]
     let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey)
     let metadata = coder.decodeObject(of: UserMetadata.self, forKey: kMetadataCodingKey)
     let tenantID = coder.decodeObject(of: NSString.self, forKey: kTenantIDCodingKey) as? String
     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)
       let multiFactor = coder.decodeObject(of: MultiFactor.self, forKey: kMultiFactorCodingKey)
     #endif
     #endif
     self.tokenService = tokenService
     self.tokenService = tokenService
@@ -1778,7 +1778,7 @@ extension User: NSSecureCoding {}
     backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())
     backend = AuthBackend(rpcIssuer: AuthBackendRPCIssuer())
 
 
     userProfileUpdate = UserProfileUpdate()
     userProfileUpdate = UserProfileUpdate()
-    #if os(iOS)
+    #if os(iOS) || os(macOS)
       self.multiFactor = multiFactor ?? MultiFactor()
       self.multiFactor = multiFactor ?? MultiFactor()
       super.init()
       super.init()
       multiFactor?.user = self
       multiFactor?.user = self

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

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

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

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

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

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

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

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

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

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

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

@@ -135,6 +135,27 @@ class UserTests: RPCBaseTests {
       ])
       ])
     #endif
     #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 = [[
     rpcIssuer?.fakeGetAccountProviderJSON = [[
       kProviderUserInfoKey: providerUserInfos,
       kProviderUserInfoKey: providerUserInfos,
       kLocalIDKey: kLocalID,
       kLocalIDKey: kLocalID,
@@ -146,21 +167,7 @@ class UserTests: RPCBaseTests {
       "phoneNumber": kPhoneNumber,
       "phoneNumber": kPhoneNumber,
       "createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds
       "createdAt": String(Int(kCreationDateTimeIntervalInSeconds) * 1000), // to nanoseconds
       "lastLoginAt": String(Int(kLastSignInDateTimeIntervalInSeconds) * 1000),
       "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)
     let expectation = self.expectation(description: #function)
@@ -247,9 +254,12 @@ class UserTests: RPCBaseTests {
         var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self,
         var encodedClasses = [User.self, NSDictionary.self, NSURL.self, SecureTokenService.self,
                               UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self,
                               UserInfoImpl.self, NSDate.self, UserMetadata.self, NSString.self,
                               NSArray.self]
                               NSArray.self]
-        #if os(iOS)
+        #if os(iOS) || os(macOS)
           encodedClasses.append(MultiFactor.self)
           encodedClasses.append(MultiFactor.self)
-          encodedClasses.append(PhoneMultiFactorInfo.self)
+          encodedClasses.append(TOTPMultiFactorInfo.self)
+          #if os(iOS)
+            encodedClasses.append(PhoneMultiFactorInfo.self)
+          #endif
         #endif
         #endif
 
 
         let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
         let unarchivedUser = try XCTUnwrap(NSKeyedUnarchiver.unarchivedObject(
@@ -370,6 +380,17 @@ class UserTests: RPCBaseTests {
             XCTAssertEqual("\(date)", kEnrolledAtMatch)
             XCTAssertEqual("\(date)", kEnrolledAtMatch)
           }
           }
         #endif
         #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 {
       } catch {
         XCTFail("Caught an error in \(#function): \(error)")
         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
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-#if os(iOS) || targetEnvironment(macCatalyst)
+#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS)
 
 
   import Combine
   import Combine
   import FirebaseAuth
   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(tvOS, unavailable)
   @available(watchOS, unavailable)
   @available(watchOS, unavailable)
   public extension MultiFactor {
   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
 // See the License for the specific language governing permissions and
 // limitations under the License.
 // limitations under the License.
 
 
-#if os(iOS) || targetEnvironment(macCatalyst)
+#if os(iOS) || targetEnvironment(macCatalyst) || os(macOS)
 
 
   import Combine
   import Combine
   import FirebaseAuth
   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(tvOS, unavailable)
   @available(watchOS, unavailable)
   @available(watchOS, unavailable)
   public extension MultiFactorResolver {
   public extension MultiFactorResolver {
@@ -43,4 +42,4 @@
     }
     }
   }
   }
 
 
-#endif // os(iOS) || targetEnvironment(macCatalyst)
+#endif // os(iOS) || targetEnvironment(macCatalyst) || os(macOS)