Skip to content

Commit a4c120d

Browse files
Merge pull request #1273 from firebase/mfa-enrollment
2 parents 9c6705b + bc4e84e commit a4c120d

25 files changed

+3388
-74
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
@preconcurrency import FirebaseAuth
15+
import SwiftUI
16+
17+
public enum SecondFactorType {
18+
case sms
19+
case totp
20+
}
21+
22+
public struct TOTPEnrollmentInfo {
23+
public let sharedSecretKey: String
24+
public let qrCodeURL: URL?
25+
public let accountName: String?
26+
public let issuer: String?
27+
public let verificationStatus: VerificationStatus
28+
29+
public enum VerificationStatus {
30+
case pending
31+
case verified
32+
case failed
33+
}
34+
35+
public init(sharedSecretKey: String,
36+
qrCodeURL: URL? = nil,
37+
accountName: String? = nil,
38+
issuer: String? = nil,
39+
verificationStatus: VerificationStatus = .pending) {
40+
self.sharedSecretKey = sharedSecretKey
41+
self.qrCodeURL = qrCodeURL
42+
self.accountName = accountName
43+
self.issuer = issuer
44+
self.verificationStatus = verificationStatus
45+
}
46+
}
47+
48+
public struct EnrollmentSession {
49+
public let id: String
50+
public let type: SecondFactorType
51+
public let session: MultiFactorSession
52+
public let totpInfo: TOTPEnrollmentInfo?
53+
public let phoneNumber: String?
54+
public let verificationId: String?
55+
public let status: EnrollmentStatus
56+
public let createdAt: Date
57+
public let expiresAt: Date
58+
59+
// Internal handle to finish TOTP
60+
internal let _totpSecret: AnyObject?
61+
62+
public enum EnrollmentStatus {
63+
case initiated
64+
case verificationSent
65+
case verificationPending
66+
case completed
67+
case failed
68+
case expired
69+
}
70+
71+
public init(id: String = UUID().uuidString,
72+
type: SecondFactorType,
73+
session: MultiFactorSession,
74+
totpInfo: TOTPEnrollmentInfo? = nil,
75+
phoneNumber: String? = nil,
76+
verificationId: String? = nil,
77+
status: EnrollmentStatus = .initiated,
78+
createdAt: Date = Date(),
79+
expiresAt: Date = Date().addingTimeInterval(600), // 10 minutes default
80+
_totpSecret: AnyObject? = nil) {
81+
self.id = id
82+
self.type = type
83+
self.session = session
84+
self.totpInfo = totpInfo
85+
self.phoneNumber = phoneNumber
86+
self.verificationId = verificationId
87+
self.status = status
88+
self.createdAt = createdAt
89+
self.expiresAt = expiresAt
90+
self._totpSecret = _totpSecret
91+
}
92+
93+
public var isExpired: Bool {
94+
return Date() > expiresAt
95+
}
96+
97+
public var canProceed: Bool {
98+
return !isExpired &&
99+
(status == .initiated || status == .verificationSent || status == .verificationPending)
100+
}
101+
}
102+
103+
public enum MFAHint {
104+
case phone(displayName: String?, uid: String, phoneNumber: String?)
105+
case totp(displayName: String?, uid: String)
106+
}
107+
108+
public struct MFARequired {
109+
public let hints: [MFAHint]
110+
111+
public init(hints: [MFAHint]) {
112+
self.hints = hints
113+
}
114+
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/AuthServiceError.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public enum AuthServiceError: LocalizedError {
3838
case accountMergeConflict(context: AccountMergeConflictContext)
3939
case invalidPhoneAuthenticationArguments(String)
4040
case providerNotFound(String)
41+
case multiFactorAuth(String)
42+
4143

4244
public var errorDescription: String? {
4345
switch self {
@@ -61,6 +63,8 @@ public enum AuthServiceError: LocalizedError {
6163
return description
6264
case let .invalidPhoneAuthenticationArguments(description):
6365
return description
66+
case let .multiFactorAuth(description):
67+
return description
6468
}
6569
}
6670
}

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthConfiguration.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,23 @@ public struct AuthConfiguration {
2525
public let emailLinkSignInActionCodeSettings: ActionCodeSettings?
2626
public let verifyEmailActionCodeSettings: ActionCodeSettings?
2727

28+
// MARK: - MFA Configuration
29+
30+
public let mfaEnabled: Bool
31+
public let allowedSecondFactors: Set<SecondFactorType>
32+
public let mfaIssuer: String
33+
2834
public init(shouldHideCancelButton: Bool = false,
2935
interactiveDismissEnabled: Bool = true,
3036
shouldAutoUpgradeAnonymousUsers: Bool = false,
3137
customStringsBundle: Bundle? = nil,
3238
tosUrl: URL? = nil,
3339
privacyPolicyUrl: URL? = nil,
3440
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
35-
verifyEmailActionCodeSettings: ActionCodeSettings? = nil) {
41+
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
42+
mfaEnabled: Bool = false,
43+
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
44+
mfaIssuer: String = "Firebase Auth") {
3645
self.shouldHideCancelButton = shouldHideCancelButton
3746
self.interactiveDismissEnabled = interactiveDismissEnabled
3847
self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers
@@ -41,5 +50,8 @@ public struct AuthConfiguration {
4150
self.privacyPolicyUrl = privacyPolicyUrl
4251
self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings
4352
self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings
53+
self.mfaEnabled = mfaEnabled
54+
self.allowedSecondFactors = allowedSecondFactors
55+
self.mfaIssuer = mfaIssuer
4456
}
4557
}

0 commit comments

Comments
 (0)