From 2d71e74c0c03b2a33268930e82d5bb0488e2d299 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Thu, 22 May 2025 12:25:52 +0100 Subject: [PATCH 1/3] feat: google reauthenticate logic for sensitive operations --- .../Sources/Services/AuthService.swift | 3 + .../Sources/Views/AuthPickerView.swift | 76 ++++++++++--------- .../Services/AccountService+Google.swift | 54 +++++++++++++ .../Services/GoogleProviderAuthUI.swift | 18 ++--- .../Views/SignInWithGoogleButton.swift | 37 +++++++++ .../FirebaseSwiftUIExample/ContentView.swift | 1 - 6 files changed, 140 insertions(+), 49 deletions(-) create mode 100644 FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift create mode 100644 FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 11339f4ca0..373d41f2d7 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -8,6 +8,7 @@ public protocol ExternalAuthProvider { public protocol GoogleProviderAuthUIProtocol: ExternalAuthProvider { @MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential + @MainActor func deleteUser(user: User) async throws } public protocol FacebookProviderAuthUIProtocol: ExternalAuthProvider { @@ -265,6 +266,8 @@ public extension AuthService { try await operation(on: user) } else if providerId == "facebook.com" { try await facebookProvider.deleteUser(user: user) + } else if providerId == "google.com" { + try await googleProvider.deleteUser(user: user) } } diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift index cb753567fb..6f5cd8789b 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift @@ -15,46 +15,48 @@ public struct AuthPickerView { extension AuthPickerView: View { public var body: some View { - VStack { - Text(authService.string.authPickerTitle) - .font(.largeTitle) - .fontWeight(.bold) - .padding() - if authService.authenticationState == .authenticated { - SignedInView() - } else if authService.authView == .passwordRecovery { - PasswordRecoveryView() - } else if authService.authView == .emailLink { - EmailLinkView() - } else { - if authService.emailSignInEnabled { - Text(authService.authenticationFlow == .login ? authService.string - .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) - Divider() - EmailAuthView() - } - VStack { - authService.renderButtons() - }.padding(.horizontal) - if authService.emailSignInEnabled { - Divider() - HStack { - Text(authService - .authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel : - authService.string.alreadyHaveAnAccountLabel) - Button(action: { - withAnimation { - switchFlow() + ScrollView { + VStack { + Text(authService.string.authPickerTitle) + .font(.largeTitle) + .fontWeight(.bold) + .padding() + if authService.authenticationState == .authenticated { + SignedInView() + } else if authService.authView == .passwordRecovery { + PasswordRecoveryView() + } else if authService.authView == .emailLink { + EmailLinkView() + } else { + if authService.emailSignInEnabled { + Text(authService.authenticationFlow == .login ? authService.string + .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) + Divider() + EmailAuthView() + } + VStack { + authService.renderButtons() + }.padding(.horizontal) + if authService.emailSignInEnabled { + Divider() + HStack { + Text(authService + .authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel : + authService.string.alreadyHaveAnAccountLabel) + Button(action: { + withAnimation { + switchFlow() + } + }) { + Text(authService.authenticationFlow == .signUp ? authService.string + .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) + .fontWeight(.semibold) + .foregroundColor(.blue) } - }) { - Text(authService.authenticationFlow == .signUp ? authService.string - .emailLoginFlowLabel : authService.string.emailSignUpFlowLabel) - .fontWeight(.semibold) - .foregroundColor(.blue) } + PrivacyTOCsView(displayMode: .footer) + Text(authService.errorMessage).foregroundColor(.red) } - PrivacyTOCsView(displayMode: .footer) - Text(authService.errorMessage).foregroundColor(.red) } } } diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift new file mode 100644 index 0000000000..827c42183f --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/AccountService+Google.swift @@ -0,0 +1,54 @@ +// +// AccountService+Google.swift +// FirebaseUI +// +// Created by Russell Wheatley on 22/05/2025. +// + +// +// AccountService+Facebook.swift +// FirebaseUI +// +// Created by Russell Wheatley on 14/05/2025. +// + +@preconcurrency import FirebaseAuth +import FirebaseAuthSwiftUI +import Observation + +protocol GoogleOperationReauthentication { + var googleProvider: GoogleProviderAuthUI { get } +} + +extension GoogleOperationReauthentication { + @MainActor func reauthenticate() async throws -> AuthenticationToken { + guard let user = Auth.auth().currentUser else { + throw AuthServiceError.reauthenticationRequired("No user currently signed-in") + } + + do { + let credential = try await googleProvider + .signInWithGoogle(clientID: googleProvider.clientID) + try await user.reauthenticate(with: credential) + + return .firebase("") + } catch { + throw AuthServiceError.signInFailed(underlying: error) + } + } +} + +@MainActor +class GoogleDeleteUserOperation: AuthenticatedOperation, + @preconcurrency GoogleOperationReauthentication { + let googleProvider: GoogleProviderAuthUI + init(googleProvider: GoogleProviderAuthUI) { + self.googleProvider = googleProvider + } + + func callAsFunction(on user: User) async throws { + try await callAsFunction(on: user) { + try await user.delete() + } + } +} diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift index 3fdde26635..af68df3b56 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -20,23 +20,19 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol let scopes: [String] let shortName = "Google" let providerId = "google.com" - let clientID: String + public let clientID: String public init(scopes: [String]? = nil, clientID: String = FirebaseApp.app()!.options.clientID!) { self.scopes = scopes ?? kDefaultScopes self.clientID = clientID } @MainActor public func authButton() -> AnyView { - let customViewModel = GoogleSignInButtonViewModel( - scheme: .light, - style: .wide, - state: .normal - ) - return AnyView(GoogleSignInButton(viewModel: customViewModel) { - Task { - try await self.signInWithGoogle(clientID: self.clientID) - } - }) + AnyView(SignInWithGoogleButton()) + } + + public func deleteUser(user: User) async throws { + let operation = GoogleDeleteUserOperation(googleProvider: self) + try await operation(on: user) } @MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential { diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift new file mode 100644 index 0000000000..4f7c97cc93 --- /dev/null +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Views/SignInWithGoogleButton.swift @@ -0,0 +1,37 @@ +// +// SignInWithGoogleButton.swift +// FirebaseUI +// +// Created by Russell Wheatley on 22/05/2025. +// +import FirebaseAuthSwiftUI +import FirebaseCore +import GoogleSignInSwift +import SwiftUI + +@MainActor +public struct SignInWithGoogleButton { + @Environment(AuthService.self) private var authService + + let customViewModel = GoogleSignInButtonViewModel( + scheme: .light, + style: .wide, + state: .normal + ) +} + +extension SignInWithGoogleButton: View { + public var body: some View { + GoogleSignInButton(viewModel: customViewModel) { + Task { + try await authService.signInWithGoogle() + } + } + } +} + +#Preview { + FirebaseOptions.dummyConfigurationForPreview() + return SignInWithGoogleButton() + .environment(AuthService()) +} diff --git a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift index f53772ae5e..6d2352809f 100644 --- a/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift +++ b/samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift @@ -25,7 +25,6 @@ struct ContentView: View { actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com" actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!) let configuration = AuthConfiguration( - shouldAutoUpgradeAnonymousUsers: true, tosUrl: URL(string: "https://example.com/tos"), privacyPolicyUrl: URL(string: "https://example.com/privacy"), emailLinkSignInActionCodeSettings: actionCodeSettings From 58a85ab6a70824786e121d9a4bd197deee05f520 Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 28 May 2025 10:20:38 +0100 Subject: [PATCH 2/3] chore: code comment --- .../Sources/Services/GoogleProviderAuthUI.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift index af68df3b56..308275f400 100644 --- a/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift +++ b/FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift @@ -27,6 +27,7 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol } @MainActor public func authButton() -> AnyView { + // Moved to SignInWithGoogleButton so we could sign in via AuthService AnyView(SignInWithGoogleButton()) } From f991f963809f91ab410a59ad0708e23e872565df Mon Sep 17 00:00:00 2001 From: russellwheatley Date: Wed, 28 May 2025 12:00:46 +0100 Subject: [PATCH 3/3] chore: use firebase-ios-sdk constants. fixed email provider not having credential in result --- .../Sources/Services/AuthService.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift index 373d41f2d7..e4f58a7609 100644 --- a/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift +++ b/FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift @@ -228,7 +228,7 @@ public final class AuthService { try await handleAutoUpgradeAnonymousUser(credentials: credentials) } else { let result = try await auth.signIn(with: credentials) - signedInCredential = result.credential + signedInCredential = result.credential ?? credentials } updateAuthenticationState() } catch { @@ -261,12 +261,12 @@ public extension AuthService { func deleteUser() async throws { do { if let user = auth.currentUser, let providerId = signedInCredential?.provider { - if providerId == "password" { + if providerId == EmailAuthProviderID { let operation = EmailPasswordDeleteUserOperation(passwordPrompt: passwordPrompt) try await operation(on: user) - } else if providerId == "facebook.com" { + } else if providerId == FacebookAuthProviderID { try await facebookProvider.deleteUser(user: user) - } else if providerId == "google.com" { + } else if providerId == GoogleAuthProviderID { try await googleProvider.deleteUser(user: user) } }