@@ -49,12 +49,15 @@ public enum AuthView {
4949 case passwordRecovery
5050 case emailLink
5151 case updatePassword
52+ case mfaEnrollment
5253}
5354
5455public enum SignInOutcome : @unchecked Sendable {
5556 case signedIn( AuthDataResult ? )
5657}
5758
59+
60+
5861@MainActor
5962private final class AuthListenerManager {
6063 private var authStateHandle : AuthStateDidChangeListenerHandle ?
@@ -517,4 +520,203 @@ public extension AuthService {
517520 throw error
518521 }
519522 }
520- }
523+ }
524+
525+ // MARK: - MFA Methods (Placeholder implementations)
526+
527+ public extension AuthService {
528+ func startMfaEnrollment( type: SecondFactorType , accountName: String ? = nil ,
529+ issuer: String ? = nil ) async throws -> EnrollmentSession {
530+ guard let user = auth. currentUser else {
531+ throw AuthServiceError . noCurrentUser
532+ }
533+
534+ // Check if MFA is enabled in configuration
535+ guard configuration. mfaEnabled else {
536+ throw AuthServiceError . multiFactorAuth ( " MFA is not enabled in configuration, please enable `AuthConfiguration.mfaEnabled` " )
537+ }
538+
539+ // Check if the requested factor type is allowed
540+ guard configuration. allowedSecondFactors. contains ( type) else {
541+ throw AuthServiceError
542+ . multiFactorAuth (
543+ " The requested MFA factor type ' \( type) ' is not allowed in AuthConfiguration.allowedSecondFactors "
544+ )
545+ }
546+
547+ let multiFactorUser = user. multiFactor
548+
549+ // Get the multi-factor session
550+ let session = try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation <
551+ MultiFactorSession ,
552+ Error
553+ > ) in
554+ multiFactorUser. getSessionWithCompletion { session, error in
555+ if let error = error {
556+ continuation. resume ( throwing: error)
557+ } else if let session = session {
558+ continuation. resume ( returning: session)
559+ } else {
560+ continuation. resume ( throwing: AuthServiceError . multiFactorAuth ( " Failed to get MFA session for ' \( type) ' " ) )
561+ }
562+ }
563+ }
564+
565+ switch type {
566+ case . sms:
567+ // For SMS, we just return the session - phone number will be provided in
568+ // sendSmsVerificationForEnrollment
569+ return EnrollmentSession (
570+ type: . sms,
571+ session: session,
572+ status: . initiated
573+ )
574+
575+ case . totp:
576+ // For TOTP, generate the secret and QR code
577+ let totpSecret = try await TOTPMultiFactorGenerator . generateSecret ( with: session)
578+
579+ // Generate QR code URL
580+ let resolvedAccountName = accountName ?? user. email ?? " User "
581+ let resolvedIssuer = issuer ?? configuration. mfaIssuer
582+
583+ let qrCodeURL = totpSecret. generateQRCodeURL (
584+ withAccountName: resolvedAccountName,
585+ issuer: resolvedIssuer
586+ )
587+
588+ let totpInfo = TOTPEnrollmentInfo (
589+ sharedSecretKey: totpSecret. sharedSecretKey ( ) ,
590+ qrCodeURL: URL ( string: qrCodeURL) ,
591+ accountName: resolvedAccountName,
592+ issuer: resolvedIssuer,
593+ verificationStatus: . pending
594+ )
595+
596+ return EnrollmentSession (
597+ type: . totp,
598+ session: session,
599+ totpInfo: totpInfo,
600+ status: . initiated,
601+ _totpSecret: totpSecret
602+ )
603+ }
604+ }
605+
606+ func sendSmsVerificationForEnrollment( session: EnrollmentSession ,
607+ phoneNumber: String ) async throws -> String {
608+ // Validate session
609+ guard session. type == . sms else {
610+ throw AuthServiceError . multiFactorAuth ( " Session is not configured for SMS enrollment " )
611+ }
612+
613+ guard session. canProceed else {
614+ if session. isExpired {
615+ throw AuthServiceError . multiFactorAuth ( " Enrollment session has expired " )
616+ } else {
617+ throw AuthServiceError
618+ . multiFactorAuth ( " Session is not in a valid state for SMS verification " )
619+ }
620+ }
621+
622+ // Validate phone number format
623+ guard !phoneNumber. isEmpty else {
624+ throw AuthServiceError . multiFactorAuth ( " Phone number cannot be empty for SMS enrollment " )
625+ }
626+
627+ // Send SMS verification using Firebase Auth PhoneAuthProvider
628+ let verificationID =
629+ try await withCheckedThrowingContinuation { ( continuation: CheckedContinuation <
630+ String ,
631+ Error
632+ > ) in
633+ PhoneAuthProvider . provider ( ) . verifyPhoneNumber (
634+ phoneNumber,
635+ uiDelegate: nil ,
636+ multiFactorSession: session. session
637+ ) { verificationID, error in
638+ if let error = error {
639+ continuation. resume ( throwing: error)
640+ } else if let verificationID = verificationID {
641+ continuation. resume ( returning: verificationID)
642+ } else {
643+ continuation
644+ . resume ( throwing: AuthServiceError
645+ . multiFactorAuth ( " Failed to send SMS verification code to verify phone number " ) )
646+ }
647+ }
648+ }
649+
650+ return verificationID
651+ }
652+
653+ func completeEnrollment( session: EnrollmentSession , verificationId: String ? ,
654+ verificationCode: String , displayName: String ) async throws {
655+ // Validate session state
656+ guard session. canProceed else {
657+ if session. isExpired {
658+ throw AuthServiceError . multiFactorAuth ( " Enrollment session has expired, cannot complete enrollment " )
659+ } else {
660+ throw AuthServiceError . multiFactorAuth ( " Enrollment session is not in a valid state for completion " )
661+ }
662+ }
663+
664+ // Validate verification code
665+ guard !verificationCode. isEmpty else {
666+ throw AuthServiceError . multiFactorAuth ( " Verification code cannot be empty " )
667+ }
668+
669+ guard let user = auth. currentUser else {
670+ throw AuthServiceError . noCurrentUser
671+ }
672+
673+ let multiFactorUser = user. multiFactor
674+
675+ // Create the appropriate assertion based on factor type
676+ let assertion : MultiFactorAssertion
677+
678+ switch session. type {
679+ case . sms:
680+ // For SMS, we need the verification ID
681+ guard let verificationId = verificationId else {
682+ throw AuthServiceError
683+ . multiFactorAuth ( " Verification ID is required for SMS enrollment " )
684+ }
685+
686+ // Create phone credential and assertion
687+ let credential = PhoneAuthProvider . provider ( ) . credential (
688+ withVerificationID: verificationId,
689+ verificationCode: verificationCode
690+ )
691+ assertion = PhoneMultiFactorGenerator . assertion ( with: credential)
692+
693+ case . totp:
694+ // For TOTP, we need the secret from the session
695+ guard let totpInfo = session. totpInfo else {
696+ throw AuthServiceError
697+ . multiFactorAuth ( " TOTP info is missing from enrollment session " )
698+ }
699+
700+ // Use the stored TOTP secret from the enrollment session
701+ guard let secret = session. _totpSecret else {
702+ throw AuthServiceError
703+ . multiFactorAuth ( " TOTP secret is missing from enrollment session " )
704+ }
705+
706+ // The concrete type is FirebaseAuth.TOTPSecret (kept as AnyObject to avoid exposing it)
707+ guard let totpSecret = secret as? TOTPSecret else {
708+ throw AuthServiceError
709+ . multiFactorAuth ( " Invalid TOTP secret type in enrollment session " )
710+ }
711+
712+ assertion = TOTPMultiFactorGenerator . assertionForEnrollment (
713+ with: totpSecret,
714+ oneTimePassword: verificationCode
715+ )
716+ }
717+
718+ // Complete the enrollment
719+ try await user. multiFactor. enroll ( with: assertion, displayName: displayName)
720+ currentUser = auth. currentUser
721+ }
722+ }
0 commit comments