diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index eca8b71f9ca..fc9b3ac63be 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -1320,10 +1320,17 @@ extension Auth: AuthInterop { /// dictionary will contain more information about the error encountered. @objc(signOut:) open func signOut() throws { try kAuthGlobalWorkQueue.sync { - guard self._currentUser != nil else { - return + if self._currentUser != nil { + // Clear standard user session if one exists. + try self.updateCurrentUser(nil, byForce: false, savingToDisk: true) + } + + // Clear R-GCIP token-only session. + self.rGCIPFirebaseTokenLock.withLock { token in + if token != nil { + token = nil + } } - return try self.updateCurrentUser(nil, byForce: false, savingToDisk: true) } } @@ -2541,13 +2548,17 @@ public extension Auth { /// call fails, or if the token response parsing fails. func exchangeToken(idToken: String, idpConfigId: String, useStaging: Bool = false) async throws -> FirebaseToken { - // Ensure R-GCIP is configured with location and tenant ID - guard let _ = requestConfiguration.tenantConfig?.location, - let _ = requestConfiguration.tenantConfig?.tenantId - else { + /// Ensure R-GCIP is configured with location and tenant ID. + guard requestConfiguration.tenantConfig != nil else { /// This should never happen in production code, as it indicates a misconfiguration. fatalError("R-GCIP is not configured correctly.") } + /// This method should only be called on an R-GCIP instance + guard _currentUser == nil else { + fatalError( + "exchangeToken cannot be called on an Auth instance with an active standard user session." + ) + } let request = ExchangeTokenRequest( idToken: idToken, idpConfigID: idpConfigId, @@ -2560,11 +2571,8 @@ public extension Auth { token: response.firebaseToken, expirationDate: response.expirationDate ) - // Lock and update the token, signing out any current user. + // Lock and update the R-GCIP token. rGCIPFirebaseTokenLock.withLock { token in - if self._currentUser != nil { - try? self.signOut() - } token = newToken } return newToken diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift index 7d37df7f323..214786b1440 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift @@ -25,25 +25,37 @@ class AppManager { private var otherApp: FirebaseApp var app: FirebaseApp - // Initialise Auth with TenantConfig - let tenantConfig = TenantConfig(tenantId: "Foo-e2e-tenant-007", location: "global") + // TenantConfig for regionalized Auth + private let tenantConfig = TenantConfig(tenantId: "Foo-e2e-tenant-007", location: "global") + + // Cached Auth instances + private var defaultAppAuth: Auth + private var otherAppAuth: Auth + private var regionalAuthInstance: Auth + func auth() -> Auth { - return Auth.auth(app: app, tenantConfig: tenantConfig) + /// Always return the specific regional instance + return regionalAuthInstance } private init() { defaultApp = FirebaseApp.app()! - app = FirebaseApp.app()! + defaultAppAuth = Auth.auth(app: defaultApp) guard let path = Bundle.main.path(forResource: "GoogleService-Info_multi", ofType: "plist"), let options = FirebaseOptions(contentsOfFile: path) else { fatalError("GoogleService-Info_multi.plist must be added to the project") } - - FirebaseApp.configure(name: "OtherApp", options: options) + if FirebaseApp.app(name: "OtherApp") == nil { + FirebaseApp.configure(name: "OtherApp", options: options) + } guard let other = FirebaseApp.app(name: "OtherApp") else { fatalError("Failed to find OtherApp") } otherApp = other + otherAppAuth = Auth.auth(app: otherApp) + regionalAuthInstance = Auth.auth(app: otherApp, tenantConfig: tenantConfig) + print("AppManager init: regionalAuthInstance created: \(regionalAuthInstance)") + app = defaultApp } func toggle() { @@ -53,4 +65,13 @@ class AppManager { app = defaultApp } } + + /// Function to get the standard Auth instance based on the current 'app' + func standardAuth() -> Auth { + if app == defaultApp { + return defaultAppAuth + } else { + return otherAppAuth + } + } } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index 0ef774cfbb0..cbcd99960d1 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -54,6 +54,7 @@ enum AuthMenu: String { case totpEnroll case multifactorUnenroll case exchangeToken + case exchangeTokenSignOut // More intuitively named getter for `rawValue`. var id: String { rawValue } @@ -143,6 +144,8 @@ enum AuthMenu: String { // R-GCIP Exchange Token case .exchangeToken: return "Exchange Token" + case .exchangeTokenSignOut: + return "Sign Out from R-GCIP" } } @@ -226,6 +229,8 @@ enum AuthMenu: String { self = .multifactorUnenroll case "Exchange Token": self = .exchangeToken + case "Sign Out from R-GCIP": + self = .exchangeTokenSignOut default: return nil } @@ -364,6 +369,7 @@ class AuthMenuData: DataSourceProvidable { let header = "Exchange Token [Regionalized Auth]" let items: [Item] = [ Item(title: AuthMenu.exchangeToken.name), + Item(title: AuthMenu.exchangeTokenSignOut.name), ] return Section(headerDescription: header, items: items) } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index d858a2c15bc..13a7d6ae720 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -194,6 +194,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .exchangeToken: callExchangeToken() + + case .exchangeTokenSignOut: + callExchangeTokenSignOut() } } @@ -1172,6 +1175,35 @@ extension AuthViewController: ASAuthorizationControllerDelegate, } } + private func callExchangeTokenSignOut() { + Task { + do { + try AppManager.shared.auth().signOut() + print("Sign out successful.") + await MainActor.run { + let alert = UIAlertController( + title: "Signed Out", + message: "The current R-GCIP session has been signed out.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + } catch { + print("Failed to sign out: \(error)") + await MainActor.run { + let alert = UIAlertController( + title: "Sign Out Error", + message: error.localizedDescription, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self.present(alert, animated: true) + } + } + } + } + // Helper function to truncate strings private func truncateString(_ string: String, maxLength: Int) -> String { if string.count > maxLength {