Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
36b1c77
feat: MFA enrolment flow
russellwheatley Oct 3, 2025
5d75816
fix: make secret internal but not private
russellwheatley Oct 3, 2025
bb34c52
feat: integrate MFA enrolment View
russellwheatley Oct 3, 2025
c5aa33a
ci: update SwiftUI CI workflow
russellwheatley Oct 3, 2025
8d9e8bc
test: unit tests for enrolment
russellwheatley Oct 3, 2025
a661857
test: integration test updates
russellwheatley Oct 3, 2025
54bb93e
test: fix UI tests
russellwheatley Oct 3, 2025
d97238b
test: remove Timer from UI tests
russellwheatley Oct 3, 2025
55f95b8
test: MFA enrolment tests
russellwheatley Oct 3, 2025
0307ebd
test: bifurcate test setup with content view + test helpers
russellwheatley Oct 3, 2025
ba2c62c
chore: clean up
russellwheatley Oct 3, 2025
0807e7c
feat: MFA management and resolution logic
russellwheatley Oct 3, 2025
1c7fed3
tests: further test setup
russellwheatley Oct 3, 2025
ca787a7
fix: back button email link
russellwheatley Oct 3, 2025
69dfd19
test: MFA resolution
russellwheatley Oct 3, 2025
da1a999
chore: clean up logs/code comments
russellwheatley Oct 3, 2025
a6b8a49
fix: catch MFA error
russellwheatley Oct 3, 2025
3c78329
chore: clean up
russellwheatley Oct 3, 2025
f4e6384
test: comment out test assertion that fails on emulator
russellwheatley Oct 6, 2025
abce28c
feat: one tap copy of TOTP secret
russellwheatley Oct 6, 2025
4763afd
fix: only show keyboard once text field has been tapped
russellwheatley Oct 6, 2025
0c88e41
fix: textfield comes into View when tapped
russellwheatley Oct 7, 2025
ce8a513
chore: previews for enrolment
russellwheatley Oct 7, 2025
c74865c
fix: ensure barcode is visible and inputs come into view when tapped
russellwheatley Oct 7, 2025
ee930ed
feat: allow opening app for creating TOTP auth
russellwheatley Oct 7, 2025
571f479
chore: display name is not optional
russellwheatley Oct 7, 2025
0c002fc
Merge branch 'development' into mfa-enrollment
russellwheatley Oct 7, 2025
36449ce
fix: change the package.swift in line with removing deprecated names
russellwheatley Oct 7, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Auth/MultiFactor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Copyright 2025 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@preconcurrency import FirebaseAuth
import SwiftUI

public enum SecondFactorType {
case sms
case totp
}

public struct TOTPEnrollmentInfo {
public let sharedSecretKey: String
public let qrCodeURL: URL?
public let accountName: String?
public let issuer: String?
public let verificationStatus: VerificationStatus

public enum VerificationStatus {
case pending
case verified
case failed
}

public init(sharedSecretKey: String,
qrCodeURL: URL? = nil,
accountName: String? = nil,
issuer: String? = nil,
verificationStatus: VerificationStatus = .pending) {
self.sharedSecretKey = sharedSecretKey
self.qrCodeURL = qrCodeURL
self.accountName = accountName
self.issuer = issuer
self.verificationStatus = verificationStatus
}
}

public struct EnrollmentSession {
public let id: String
public let type: SecondFactorType
public let session: MultiFactorSession
public let totpInfo: TOTPEnrollmentInfo?
public let phoneNumber: String?
public let verificationId: String?
public let status: EnrollmentStatus
public let createdAt: Date
public let expiresAt: Date

// Internal handle to finish TOTP
internal let _totpSecret: AnyObject?

public enum EnrollmentStatus {
case initiated
case verificationSent
case verificationPending
case completed
case failed
case expired
}

public init(id: String = UUID().uuidString,
type: SecondFactorType,
session: MultiFactorSession,
totpInfo: TOTPEnrollmentInfo? = nil,
phoneNumber: String? = nil,
verificationId: String? = nil,
status: EnrollmentStatus = .initiated,
createdAt: Date = Date(),
expiresAt: Date = Date().addingTimeInterval(600), // 10 minutes default
_totpSecret: AnyObject? = nil) {
self.id = id
self.type = type
self.session = session
self.totpInfo = totpInfo
self.phoneNumber = phoneNumber
self.verificationId = verificationId
self.status = status
self.createdAt = createdAt
self.expiresAt = expiresAt
self._totpSecret = _totpSecret
}

public var isExpired: Bool {
return Date() > expiresAt
}

public var canProceed: Bool {
return !isExpired &&
(status == .initiated || status == .verificationSent || status == .verificationPending)
}
}

public enum MFAHint {
case phone(displayName: String?, uid: String, phoneNumber: String?)
case totp(displayName: String?, uid: String)
}

public struct MFARequired {
public let hints: [MFAHint]

public init(hints: [MFAHint]) {
self.hints = hints
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ public enum AuthServiceError: LocalizedError {
case accountMergeConflict(context: AccountMergeConflictContext)
case invalidPhoneAuthenticationArguments(String)
case providerNotFound(String)
case multiFactorAuth(String)


public var errorDescription: String? {
switch self {
Expand All @@ -61,6 +63,8 @@ public enum AuthServiceError: LocalizedError {
return description
case let .invalidPhoneAuthenticationArguments(description):
return description
case let .multiFactorAuth(description):
return description
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,23 @@ public struct AuthConfiguration {
public let emailLinkSignInActionCodeSettings: ActionCodeSettings?
public let verifyEmailActionCodeSettings: ActionCodeSettings?

// MARK: - MFA Configuration

public let mfaEnabled: Bool
public let allowedSecondFactors: Set<SecondFactorType>
public let mfaIssuer: String

public init(shouldHideCancelButton: Bool = false,
interactiveDismissEnabled: Bool = true,
shouldAutoUpgradeAnonymousUsers: Bool = false,
customStringsBundle: Bundle? = nil,
tosUrl: URL? = nil,
privacyPolicyUrl: URL? = nil,
emailLinkSignInActionCodeSettings: ActionCodeSettings? = nil,
verifyEmailActionCodeSettings: ActionCodeSettings? = nil) {
verifyEmailActionCodeSettings: ActionCodeSettings? = nil,
mfaEnabled: Bool = false,
allowedSecondFactors: Set<SecondFactorType> = [.sms, .totp],
mfaIssuer: String = "Firebase Auth") {
self.shouldHideCancelButton = shouldHideCancelButton
self.interactiveDismissEnabled = interactiveDismissEnabled
self.shouldAutoUpgradeAnonymousUsers = shouldAutoUpgradeAnonymousUsers
Expand All @@ -41,5 +50,8 @@ public struct AuthConfiguration {
self.privacyPolicyUrl = privacyPolicyUrl
self.emailLinkSignInActionCodeSettings = emailLinkSignInActionCodeSettings
self.verifyEmailActionCodeSettings = verifyEmailActionCodeSettings
self.mfaEnabled = mfaEnabled
self.allowedSecondFactors = allowedSecondFactors
self.mfaIssuer = mfaIssuer
}
}
Loading
Loading