Skip to content

Commit 2d71e74

Browse files
feat: google reauthenticate logic for sensitive operations
1 parent 0a5962f commit 2d71e74

File tree

6 files changed

+140
-49
lines changed

6 files changed

+140
-49
lines changed

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Services/AuthService.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ public protocol ExternalAuthProvider {
88

99
public protocol GoogleProviderAuthUIProtocol: ExternalAuthProvider {
1010
@MainActor func signInWithGoogle(clientID: String) async throws -> AuthCredential
11+
@MainActor func deleteUser(user: User) async throws
1112
}
1213

1314
public protocol FacebookProviderAuthUIProtocol: ExternalAuthProvider {
@@ -265,6 +266,8 @@ public extension AuthService {
265266
try await operation(on: user)
266267
} else if providerId == "facebook.com" {
267268
try await facebookProvider.deleteUser(user: user)
269+
} else if providerId == "google.com" {
270+
try await googleProvider.deleteUser(user: user)
268271
}
269272
}
270273

FirebaseSwiftUI/FirebaseAuthSwiftUI/Sources/Views/AuthPickerView.swift

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,48 @@ public struct AuthPickerView {
1515

1616
extension AuthPickerView: View {
1717
public var body: some View {
18-
VStack {
19-
Text(authService.string.authPickerTitle)
20-
.font(.largeTitle)
21-
.fontWeight(.bold)
22-
.padding()
23-
if authService.authenticationState == .authenticated {
24-
SignedInView()
25-
} else if authService.authView == .passwordRecovery {
26-
PasswordRecoveryView()
27-
} else if authService.authView == .emailLink {
28-
EmailLinkView()
29-
} else {
30-
if authService.emailSignInEnabled {
31-
Text(authService.authenticationFlow == .login ? authService.string
32-
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
33-
Divider()
34-
EmailAuthView()
35-
}
36-
VStack {
37-
authService.renderButtons()
38-
}.padding(.horizontal)
39-
if authService.emailSignInEnabled {
40-
Divider()
41-
HStack {
42-
Text(authService
43-
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
44-
authService.string.alreadyHaveAnAccountLabel)
45-
Button(action: {
46-
withAnimation {
47-
switchFlow()
18+
ScrollView {
19+
VStack {
20+
Text(authService.string.authPickerTitle)
21+
.font(.largeTitle)
22+
.fontWeight(.bold)
23+
.padding()
24+
if authService.authenticationState == .authenticated {
25+
SignedInView()
26+
} else if authService.authView == .passwordRecovery {
27+
PasswordRecoveryView()
28+
} else if authService.authView == .emailLink {
29+
EmailLinkView()
30+
} else {
31+
if authService.emailSignInEnabled {
32+
Text(authService.authenticationFlow == .login ? authService.string
33+
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
34+
Divider()
35+
EmailAuthView()
36+
}
37+
VStack {
38+
authService.renderButtons()
39+
}.padding(.horizontal)
40+
if authService.emailSignInEnabled {
41+
Divider()
42+
HStack {
43+
Text(authService
44+
.authenticationFlow == .login ? authService.string.dontHaveAnAccountYetLabel :
45+
authService.string.alreadyHaveAnAccountLabel)
46+
Button(action: {
47+
withAnimation {
48+
switchFlow()
49+
}
50+
}) {
51+
Text(authService.authenticationFlow == .signUp ? authService.string
52+
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
53+
.fontWeight(.semibold)
54+
.foregroundColor(.blue)
4855
}
49-
}) {
50-
Text(authService.authenticationFlow == .signUp ? authService.string
51-
.emailLoginFlowLabel : authService.string.emailSignUpFlowLabel)
52-
.fontWeight(.semibold)
53-
.foregroundColor(.blue)
5456
}
57+
PrivacyTOCsView(displayMode: .footer)
58+
Text(authService.errorMessage).foregroundColor(.red)
5559
}
56-
PrivacyTOCsView(displayMode: .footer)
57-
Text(authService.errorMessage).foregroundColor(.red)
5860
}
5961
}
6062
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// AccountService+Google.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 22/05/2025.
6+
//
7+
8+
//
9+
// AccountService+Facebook.swift
10+
// FirebaseUI
11+
//
12+
// Created by Russell Wheatley on 14/05/2025.
13+
//
14+
15+
@preconcurrency import FirebaseAuth
16+
import FirebaseAuthSwiftUI
17+
import Observation
18+
19+
protocol GoogleOperationReauthentication {
20+
var googleProvider: GoogleProviderAuthUI { get }
21+
}
22+
23+
extension GoogleOperationReauthentication {
24+
@MainActor func reauthenticate() async throws -> AuthenticationToken {
25+
guard let user = Auth.auth().currentUser else {
26+
throw AuthServiceError.reauthenticationRequired("No user currently signed-in")
27+
}
28+
29+
do {
30+
let credential = try await googleProvider
31+
.signInWithGoogle(clientID: googleProvider.clientID)
32+
try await user.reauthenticate(with: credential)
33+
34+
return .firebase("")
35+
} catch {
36+
throw AuthServiceError.signInFailed(underlying: error)
37+
}
38+
}
39+
}
40+
41+
@MainActor
42+
class GoogleDeleteUserOperation: AuthenticatedOperation,
43+
@preconcurrency GoogleOperationReauthentication {
44+
let googleProvider: GoogleProviderAuthUI
45+
init(googleProvider: GoogleProviderAuthUI) {
46+
self.googleProvider = googleProvider
47+
}
48+
49+
func callAsFunction(on user: User) async throws {
50+
try await callAsFunction(on: user) {
51+
try await user.delete()
52+
}
53+
}
54+
}

FirebaseSwiftUI/FirebaseGoogleSwiftUI/Sources/Services/GoogleProviderAuthUI.swift

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,19 @@ public class GoogleProviderAuthUI: @preconcurrency GoogleProviderAuthUIProtocol
2020
let scopes: [String]
2121
let shortName = "Google"
2222
let providerId = "google.com"
23-
let clientID: String
23+
public let clientID: String
2424
public init(scopes: [String]? = nil, clientID: String = FirebaseApp.app()!.options.clientID!) {
2525
self.scopes = scopes ?? kDefaultScopes
2626
self.clientID = clientID
2727
}
2828

2929
@MainActor public func authButton() -> AnyView {
30-
let customViewModel = GoogleSignInButtonViewModel(
31-
scheme: .light,
32-
style: .wide,
33-
state: .normal
34-
)
35-
return AnyView(GoogleSignInButton(viewModel: customViewModel) {
36-
Task {
37-
try await self.signInWithGoogle(clientID: self.clientID)
38-
}
39-
})
30+
AnyView(SignInWithGoogleButton())
31+
}
32+
33+
public func deleteUser(user: User) async throws {
34+
let operation = GoogleDeleteUserOperation(googleProvider: self)
35+
try await operation(on: user)
4036
}
4137

4238
@MainActor public func signInWithGoogle(clientID: String) async throws -> AuthCredential {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//
2+
// SignInWithGoogleButton.swift
3+
// FirebaseUI
4+
//
5+
// Created by Russell Wheatley on 22/05/2025.
6+
//
7+
import FirebaseAuthSwiftUI
8+
import FirebaseCore
9+
import GoogleSignInSwift
10+
import SwiftUI
11+
12+
@MainActor
13+
public struct SignInWithGoogleButton {
14+
@Environment(AuthService.self) private var authService
15+
16+
let customViewModel = GoogleSignInButtonViewModel(
17+
scheme: .light,
18+
style: .wide,
19+
state: .normal
20+
)
21+
}
22+
23+
extension SignInWithGoogleButton: View {
24+
public var body: some View {
25+
GoogleSignInButton(viewModel: customViewModel) {
26+
Task {
27+
try await authService.signInWithGoogle()
28+
}
29+
}
30+
}
31+
}
32+
33+
#Preview {
34+
FirebaseOptions.dummyConfigurationForPreview()
35+
return SignInWithGoogleButton()
36+
.environment(AuthService())
37+
}

samples/swiftui/FirebaseSwiftUIExample/FirebaseSwiftUIExample/ContentView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ struct ContentView: View {
2525
actionCodeSettings.linkDomain = "flutterfire-e2e-tests.firebaseapp.com"
2626
actionCodeSettings.setIOSBundleID(Bundle.main.bundleIdentifier!)
2727
let configuration = AuthConfiguration(
28-
shouldAutoUpgradeAnonymousUsers: true,
2928
tosUrl: URL(string: "https://example.com/tos"),
3029
privacyPolicyUrl: URL(string: "https://example.com/privacy"),
3130
emailLinkSignInActionCodeSettings: actionCodeSettings

0 commit comments

Comments
 (0)