@@ -50,9 +50,12 @@ public enum AuthView {
5050 case emailLink
5151 case updatePassword
5252 case mfaEnrollment
53+ case mfaManagement
54+ case mfaResolution
5355}
5456
5557public enum SignInOutcome : @unchecked Sendable {
58+ case mfaRequired( MFARequired )
5659 case signedIn( AuthDataResult ? )
5760}
5861
@@ -104,6 +107,8 @@ public final class AuthService {
104107 public var authenticationFlow : AuthenticationFlow = . signIn
105108 public var errorMessage = " "
106109 public let passwordPrompt : PasswordPromptCoordinator = . init( )
110+ public var currentMFARequired : MFARequired ?
111+ private var currentMFAResolver : MultiFactorResolver ?
107112
108113 // MARK: - AuthPickerView Modal APIs
109114
@@ -719,4 +724,169 @@ public extension AuthService {
719724 try await user. multiFactor. enroll ( with: assertion, displayName: displayName)
720725 currentUser = auth. currentUser
721726 }
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+ }
722892}
0 commit comments