@@ -50,9 +50,12 @@ public enum AuthView {
50
50
case emailLink
51
51
case updatePassword
52
52
case mfaEnrollment
53
+ case mfaManagement
54
+ case mfaResolution
53
55
}
54
56
55
57
public enum SignInOutcome : @unchecked Sendable {
58
+ case mfaRequired( MFARequired )
56
59
case signedIn( AuthDataResult ? )
57
60
}
58
61
@@ -104,6 +107,8 @@ public final class AuthService {
104
107
public var authenticationFlow : AuthenticationFlow = . signIn
105
108
public var errorMessage = " "
106
109
public let passwordPrompt : PasswordPromptCoordinator = . init( )
110
+ public var currentMFARequired : MFARequired ?
111
+ private var currentMFAResolver : MultiFactorResolver ?
107
112
108
113
// MARK: - AuthPickerView Modal APIs
109
114
@@ -719,4 +724,169 @@ public extension AuthService {
719
724
try await user. multiFactor. enroll ( with: assertion, displayName: displayName)
720
725
currentUser = auth. currentUser
721
726
}
727
+
728
+ func reauthenticateCurrentUser( on user: User ) async throws {
729
+ if let providerId = signedInCredential? . provider {
730
+ if providerId == EmailAuthProviderID {
731
+ guard let email = user. email else {
732
+ throw AuthServiceError . invalidCredentials ( " User does not have an email address " )
733
+ }
734
+ let password = try await passwordPrompt. confirmPassword ( )
735
+ let credential = EmailAuthProvider . credential ( withEmail: email, password: password)
736
+ try await user. reauthenticate ( with: credential)
737
+ } else if let matchingProvider = providers. first ( where: { $0. id == providerId } ) {
738
+ let credential = try await matchingProvider. provider. createAuthCredential ( )
739
+ try await user. reauthenticate ( with: credential)
740
+ } else {
741
+ throw AuthServiceError . providerNotFound ( " No provider found for \( providerId) " )
742
+ }
743
+ } else {
744
+ throw AuthServiceError
745
+ . reauthenticationRequired ( " Recent login required to perform this operation. " )
746
+ }
747
+ }
748
+
749
+ func unenrollMFA( _ factorUid: String ) async throws -> [ MultiFactorInfo ] {
750
+ guard let user = auth. currentUser else {
751
+ throw AuthServiceError . noCurrentUser
752
+ }
753
+
754
+ let multiFactorUser = user. multiFactor
755
+
756
+ do {
757
+ try await multiFactorUser. unenroll ( withFactorUID: factorUid)
758
+ } catch let error as NSError {
759
+ if error. domain == AuthErrorDomain,
760
+ error. code == AuthErrorCode . requiresRecentLogin. rawValue || error. code == AuthErrorCode
761
+ . userTokenExpired. rawValue {
762
+ try await reauthenticateCurrentUser ( on: user)
763
+ try await multiFactorUser. unenroll ( withFactorUID: factorUid)
764
+ } else {
765
+ throw AuthServiceError
766
+ . multiFactorAuth (
767
+ " Invalid second factor: \( error. localizedDescription) "
768
+ )
769
+ }
770
+ }
771
+
772
+ // This is the only we to get the actual latest enrolledFactors
773
+ currentUser = Auth . auth ( ) . currentUser
774
+ let freshFactors = currentUser? . multiFactor. enrolledFactors ?? [ ]
775
+
776
+ return freshFactors
777
+ }
778
+
779
+ // MARK: - MFA Helper Methods
780
+
781
+ private func extractMFAHints( from resolver: MultiFactorResolver ) -> [ MFAHint ] {
782
+ return resolver. hints. map { hint -> MFAHint in
783
+ if hint. factorID == PhoneMultiFactorID {
784
+ let phoneHint = hint as! PhoneMultiFactorInfo
785
+ return . phone(
786
+ displayName: phoneHint. displayName,
787
+ uid: phoneHint. uid,
788
+ phoneNumber: phoneHint. phoneNumber
789
+ )
790
+ } else if hint. factorID == TOTPMultiFactorID {
791
+ return . totp(
792
+ displayName: hint. displayName,
793
+ uid: hint. uid
794
+ )
795
+ } else {
796
+ // Fallback for unknown hint types
797
+ return . totp( displayName: hint. displayName, uid: hint. uid)
798
+ }
799
+ }
800
+ }
801
+
802
+ private func handleMFARequiredError( resolver: MultiFactorResolver ) -> SignInOutcome {
803
+ let hints = extractMFAHints ( from: resolver)
804
+ currentMFARequired = MFARequired ( hints: hints)
805
+ currentMFAResolver = resolver
806
+ authView = . mfaResolution
807
+ return . mfaRequired( MFARequired ( hints: hints) )
808
+ }
809
+
810
+ func resolveSmsChallenge( hintIndex: Int ) async throws -> String {
811
+ guard let resolver = currentMFAResolver else {
812
+ throw AuthServiceError . multiFactorAuth ( " No MFA resolver available " )
813
+ }
814
+
815
+ guard hintIndex < resolver. hints. count else {
816
+ throw AuthServiceError . multiFactorAuth ( " Invalid hint index " )
817
+ }
818
+
819
+ let hint = resolver. hints [ hintIndex]
820
+ guard hint. factorID == PhoneMultiFactorID else {
821
+ throw AuthServiceError . multiFactorAuth ( " Selected hint is not a phone hint " )
822
+ }
823
+ let phoneHint = hint as! PhoneMultiFactorInfo
824
+
825
+ return try await withCheckedThrowingContinuation { continuation in
826
+ PhoneAuthProvider . provider ( ) . verifyPhoneNumber (
827
+ with: phoneHint,
828
+ uiDelegate: nil ,
829
+ multiFactorSession: resolver. session
830
+ ) { verificationId, error in
831
+ if let error = error {
832
+ continuation
833
+ . resume ( throwing: AuthServiceError . multiFactorAuth ( error. localizedDescription) )
834
+ } else if let verificationId = verificationId {
835
+ continuation. resume ( returning: verificationId)
836
+ } else {
837
+ continuation
838
+ . resume ( throwing: AuthServiceError . multiFactorAuth ( " Unknown error occurred " ) )
839
+ }
840
+ }
841
+ }
842
+ }
843
+
844
+ func resolveSignIn( code: String , hintIndex: Int , verificationId: String ? = nil ) async throws {
845
+ guard let resolver = currentMFAResolver else {
846
+ throw AuthServiceError . multiFactorAuth ( " No MFA resolver available " )
847
+ }
848
+
849
+ guard hintIndex < resolver. hints. count else {
850
+ throw AuthServiceError . multiFactorAuth ( " Invalid hint index " )
851
+ }
852
+
853
+ let hint = resolver. hints [ hintIndex]
854
+ let assertion : MultiFactorAssertion
855
+
856
+ // Create the appropriate assertion based on the hint type
857
+ if hint. factorID == PhoneMultiFactorID {
858
+ guard let verificationId = verificationId else {
859
+ throw AuthServiceError . multiFactorAuth ( " Verification ID is required for SMS MFA " )
860
+ }
861
+
862
+ let credential = PhoneAuthProvider . provider ( ) . credential (
863
+ withVerificationID: verificationId,
864
+ verificationCode: code
865
+ )
866
+ assertion = PhoneMultiFactorGenerator . assertion ( with: credential)
867
+
868
+ } else if hint. factorID == TOTPMultiFactorID {
869
+ assertion = TOTPMultiFactorGenerator . assertionForSignIn (
870
+ withEnrollmentID: hint. uid,
871
+ oneTimePassword: code
872
+ )
873
+
874
+ } else {
875
+ throw AuthServiceError . multiFactorAuth ( " Unsupported MFA hint type " )
876
+ }
877
+
878
+ do {
879
+ let result = try await resolver. resolveSignIn ( with: assertion)
880
+ signedInCredential = result. credential
881
+ updateAuthenticationState ( )
882
+
883
+ // Clear MFA resolution state
884
+ currentMFARequired = nil
885
+ currentMFAResolver = nil
886
+
887
+ } catch {
888
+ throw AuthServiceError
889
+ . multiFactorAuth ( " Failed to resolve MFA challenge: \( error. localizedDescription) " )
890
+ }
891
+ }
722
892
}
0 commit comments