Skip to content

Commit ff8ff47

Browse files
Merge pull request #1290 from firebase/reauth-mfa
2 parents a4c43be + 046e936 commit ff8ff47

File tree

10 files changed

+40
-210
lines changed

10 files changed

+40
-210
lines changed

FirebaseSwiftUI/FirebaseAppleSwiftUI/Sources/Services/AppleProviderAuthUI.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -137,11 +137,6 @@ public class AppleProviderSwift: AuthProviderSwift {
137137

138138
return credential
139139
}
140-
141-
public func deleteUser(user: User) async throws {
142-
let operation = ProviderDeleteUserOperation(provider: self)
143-
try await operation(on: user)
144-
}
145140
}
146141

147142
public class AppleProviderAuthUI: AuthProviderUI {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService+Email.swift

Lines changed: 1 addition & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -12,68 +12,9 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
@preconcurrency import FirebaseAuth
1615
import Observation
1716

18-
@MainActor
19-
protocol EmailPasswordOperationReauthentication {
20-
var passwordPrompt: PasswordPromptCoordinator { get }
21-
}
22-
23-
extension EmailPasswordOperationReauthentication {
24-
func reauthenticate() async throws {
25-
guard let user = Auth.auth().currentUser else {
26-
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
27-
}
28-
29-
guard let email = user.email else {
30-
throw AuthServiceError.invalidCredentials("User does not have an email address")
31-
}
32-
33-
do {
34-
let password = try await passwordPrompt.confirmPassword()
35-
36-
let credential = EmailAuthProvider.credential(withEmail: email, password: password)
37-
_ = try await Auth.auth().currentUser?.reauthenticate(with: credential)
38-
} catch {
39-
throw AuthServiceError.signInFailed(underlying: error)
40-
}
41-
}
42-
}
43-
44-
@MainActor
45-
class EmailPasswordDeleteUserOperation: AuthenticatedOperation,
46-
EmailPasswordOperationReauthentication {
47-
let passwordPrompt: PasswordPromptCoordinator
48-
49-
init(passwordPrompt: PasswordPromptCoordinator) {
50-
self.passwordPrompt = passwordPrompt
51-
}
52-
53-
func callAsFunction(on user: User) async throws {
54-
try await callAsFunction(on: user) {
55-
try await user.delete()
56-
}
57-
}
58-
}
59-
60-
class EmailPasswordUpdatePasswordOperation: AuthenticatedOperation,
61-
EmailPasswordOperationReauthentication {
62-
let passwordPrompt: PasswordPromptCoordinator
63-
let newPassword: String
64-
65-
init(passwordPrompt: PasswordPromptCoordinator, newPassword: String) {
66-
self.passwordPrompt = passwordPrompt
67-
self.newPassword = newPassword
68-
}
69-
70-
func callAsFunction(on user: User) async throws {
71-
try await callAsFunction(on: user) {
72-
try await user.updatePassword(to: newPassword)
73-
}
74-
}
75-
}
76-
17+
/// Coordinator for prompting users to enter their password during reauthentication flows
7718
@MainActor
7819
@Observable
7920
public final class PasswordPromptCoordinator {

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AccountService.swift

Lines changed: 0 additions & 85 deletions
This file was deleted.

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 37 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import SwiftUI
1818

1919
public protocol AuthProviderSwift {
2020
@MainActor func createAuthCredential() async throws -> AuthCredential
21-
@MainActor func deleteUser(user: User) async throws
2221
}
2322

2423
public protocol AuthProviderUI {
@@ -188,7 +187,13 @@ public final class AuthService {
188187
public func linkAccounts(credentials credentials: AuthCredential) async throws {
189188
authenticationState = .authenticating
190189
do {
191-
try await currentUser?.link(with: credentials)
190+
guard let user = currentUser else {
191+
throw AuthServiceError.noCurrentUser
192+
}
193+
194+
try await withReauthenticationIfNeeded(on: user) {
195+
try await user.link(with: credentials)
196+
}
192197
updateAuthenticationState()
193198
} catch {
194199
authenticationState = .unauthenticated
@@ -277,18 +282,12 @@ public final class AuthService {
277282
public extension AuthService {
278283
func deleteUser() async throws {
279284
do {
280-
if let user = auth.currentUser, let providerId = signedInCredential?.provider {
281-
if providerId == EmailAuthProviderID {
282-
let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt)
283-
try await operation(on: user)
284-
} else {
285-
// Find provider by matching ID
286-
guard let matchingProvider = providers.first(where: { $0.id == providerId }) else {
287-
throw AuthServiceError.providerNotFound("No provider found for \(providerId)")
288-
}
285+
guard let user = auth.currentUser else {
286+
throw AuthServiceError.noCurrentUser
287+
}
289288

290-
try await matchingProvider.provider.deleteUser(user: user)
291-
}
289+
try await withReauthenticationIfNeeded(on: user) {
290+
try await user.delete()
292291
}
293292
} catch {
294293
updateError(message: string.localizedErrorMessage(for: error))
@@ -298,14 +297,13 @@ public extension AuthService {
298297

299298
func updatePassword(to password: String) async throws {
300299
do {
301-
if let user = auth.currentUser {
302-
let operation = EmailPasswordUpdatePasswordOperation(
303-
passwordPrompt: passwordPrompt,
304-
newPassword: password
305-
)
306-
try await operation(on: user)
300+
guard let user = auth.currentUser else {
301+
throw AuthServiceError.noCurrentUser
307302
}
308303

304+
try await withReauthenticationIfNeeded(on: user) {
305+
try await user.updatePassword(to: password)
306+
}
309307
} catch {
310308
updateError(message: string.localizedErrorMessage(for: error))
311309
throw error
@@ -702,7 +700,9 @@ public extension AuthService {
702700
}
703701

704702
// Complete the enrollment
705-
try await user.multiFactor.enroll(with: assertion, displayName: displayName)
703+
try await withReauthenticationIfNeeded(on: user) {
704+
try await user.multiFactor.enroll(with: assertion, displayName: displayName)
705+
}
706706
currentUser = auth.currentUser
707707
} catch {
708708
updateError(message: string.localizedErrorMessage(for: error))
@@ -731,6 +731,22 @@ public extension AuthService {
731731
}
732732
}
733733

734+
private func withReauthenticationIfNeeded(on user: User,
735+
operation: () async throws -> Void) async throws {
736+
do {
737+
try await operation()
738+
} catch let error as NSError {
739+
if error.domain == AuthErrorDomain,
740+
error.code == AuthErrorCode.requiresRecentLogin.rawValue || error.code == AuthErrorCode
741+
.userTokenExpired.rawValue {
742+
try await reauthenticateCurrentUser(on: user)
743+
try await operation()
744+
} else {
745+
throw error
746+
}
747+
}
748+
}
749+
734750
func unenrollMFA(_ factorUid: String) async throws -> [MultiFactorInfo] {
735751
do {
736752
guard let user = auth.currentUser else {
@@ -739,20 +755,8 @@ public extension AuthService {
739755

740756
let multiFactorUser = user.multiFactor
741757

742-
do {
758+
try await withReauthenticationIfNeeded(on: user) {
743759
try await multiFactorUser.unenroll(withFactorUID: factorUid)
744-
} catch let error as NSError {
745-
if error.domain == AuthErrorDomain,
746-
error.code == AuthErrorCode.requiresRecentLogin.rawValue || error.code == AuthErrorCode
747-
.userTokenExpired.rawValue {
748-
try await reauthenticateCurrentUser(on: user)
749-
try await multiFactorUser.unenroll(withFactorUID: factorUid)
750-
} else {
751-
throw AuthServiceError
752-
.multiFactorAuth(
753-
"Invalid second factor: \(error.localizedDescription)"
754-
)
755-
}
756760
}
757761

758762
// This is the only we to get the actual latest enrolledFactors

FirebaseSwiftUI/FirebaseFacebookSwiftUI/Sources/Services/FacebookProviderAuthUI.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,11 +116,6 @@ public class FacebookProviderSwift: AuthProviderSwift {
116116
)
117117
}
118118
}
119-
120-
public func deleteUser(user: User) async throws {
121-
let operation = ProviderDeleteUserOperation(provider: self)
122-
try await operation(on: user)
123-
}
124119
}
125120

126121
public class FacebookProviderAuthUI: AuthProviderUI {

FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,11 +68,6 @@ public class GoogleProviderSwift: AuthProviderSwift {
6868
}
6969
}
7070
}
71-
72-
public func deleteUser(user: User) async throws {
73-
let operation = ProviderDeleteUserOperation(provider: self)
74-
try await operation(on: user)
75-
}
7671
}
7772

7873
public class GoogleProviderAuthUI: AuthProviderUI {

FirebaseSwiftUI/FirebaseOAuthSwiftUI/Sources/Services/OAuthProviderSwift.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,6 @@ public class OAuthProviderSwift: AuthProviderSwift {
113113
}
114114
}
115115
}
116-
117-
public func deleteUser(user: User) async throws {
118-
let operation = ProviderDeleteUserOperation(provider: self)
119-
try await operation(on: user)
120-
}
121116
}
122117

123118
public class OAuthProviderAuthUI: AuthProviderUI {

FirebaseSwiftUI/FirebasePhoneAuthSwiftUI/Sources/Services/PhoneAuthProviderAuthUI.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,6 @@ public class PhoneProviderSwift: PhoneAuthProviderSwift {
6363
presentingViewController.present(hostingController, animated: true)
6464
}
6565
}
66-
67-
public func deleteUser(user: User) async throws {
68-
let operation = ProviderDeleteUserOperation(provider: self)
69-
try await operation(on: user)
70-
}
7166
}
7267

7368
public class PhoneAuthProviderAuthUI: AuthProviderUI {

FirebaseSwiftUI/FirebaseTwitterSwiftUI/Sources/Services/TwitterProviderAuthUI.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,6 @@ public class TwitterProviderSwift: AuthProviderSwift {
4242
}
4343
}
4444
}
45-
46-
public func deleteUser(user: User) async throws {
47-
let operation = ProviderDeleteUserOperation(provider: self)
48-
try await operation(on: user)
49-
}
5045
}
5146

5247
public class TwitterProviderAuthUI: AuthProviderUI {

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExampleUITests/MFAEnrolmentUITests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ final class MFAEnrollmentUITests: XCTestCase {
400400
}
401401

402402
// MARK: - Helper Methods
403-
403+
@MainActor
404404
private func signInToApp(app: XCUIApplication, email: String) throws {
405405
let password = "123456"
406406

@@ -436,7 +436,7 @@ final class MFAEnrollmentUITests: XCTestCase {
436436
XCTAssertTrue(signedInText.waitForExistence(timeout: 30), "SignedInView should be visible after login")
437437
XCTAssertTrue(signedInText.exists, "SignedInView should be visible after login")
438438
}
439-
439+
@MainActor
440440
private func navigateToMFAEnrollment(app: XCUIApplication) throws {
441441
// Navigate to MFA management
442442
app.buttons["mfa-management-button"].tap()

0 commit comments

Comments
 (0)