@@ -18,6 +18,7 @@ import FirebaseAppCheckInterop
18
18
import FirebaseAuthInterop
19
19
import FirebaseCore
20
20
import FirebaseCoreExtension
21
+ import FirebaseCoreInternal
21
22
#if COCOAPODS
22
23
internal import GoogleUtilities
23
24
#else
@@ -83,48 +84,74 @@ extension Auth: AuthInterop {
83
84
public func getToken( forcingRefresh forceRefresh: Bool ,
84
85
completion callback: @escaping ( String ? , Error ? ) -> Void ) {
85
86
kAuthGlobalWorkQueue. async { [ weak self] in
86
- if let strongSelf = self {
87
- // Enable token auto-refresh if not already enabled.
88
- if !strongSelf. autoRefreshTokens {
89
- AuthLog . logInfo ( code: " I-AUT000002 " , message: " Token auto-refresh enabled. " )
90
- strongSelf. autoRefreshTokens = true
91
- strongSelf. scheduleAutoTokenRefresh ( )
92
-
93
- #if os(iOS) || os(tvOS) // TODO(ObjC): Is a similar mechanism needed on macOS?
94
- strongSelf. applicationDidBecomeActiveObserver =
95
- NotificationCenter . default. addObserver (
96
- forName: UIApplication . didBecomeActiveNotification,
97
- object: nil , queue: nil
98
- ) { notification in
99
- if let strongSelf = self {
100
- strongSelf. isAppInBackground = false
101
- if !strongSelf. autoRefreshScheduled {
102
- strongSelf. scheduleAutoTokenRefresh ( )
103
- }
104
- }
105
- }
106
- strongSelf. applicationDidEnterBackgroundObserver =
107
- NotificationCenter . default. addObserver (
108
- forName: UIApplication . didEnterBackgroundNotification,
109
- object: nil , queue: nil
110
- ) { notification in
111
- if let strongSelf = self {
112
- strongSelf. isAppInBackground = true
113
- }
114
- }
115
- #endif
87
+ guard let self else {
88
+ DispatchQueue . main. async { callback ( nil , nil ) }
89
+ return
90
+ }
91
+ /// Before checking for a standard user, check if we are in a token-only session established
92
+ /// by a successful exchangeToken call.
93
+ let rGCIPToken = self . rGCIPFirebaseTokenLock. withLock { $0 }
94
+
95
+ if let token = rGCIPToken {
96
+ /// Logic for tokens obtained via exchangeToken (R-GCIP mode)
97
+ if token. expirationDate < Date ( ) {
98
+ /// Token expired
99
+ let error = AuthErrorUtils
100
+ . userTokenExpiredError (
101
+ message: " The firebase access token obtained via exchangeToken() has expired. "
102
+ )
103
+ Auth . wrapMainAsync ( callback: callback, with: . failure( error) )
104
+ } else if forceRefresh {
105
+ /// Token is not expired, but forceRefresh was requested which is currently unsupported
106
+ let error = AuthErrorUtils
107
+ . operationNotAllowedError (
108
+ message: " forceRefresh is not supported for firebase access tokens obtained via exchangeToken(). "
109
+ )
110
+ Auth . wrapMainAsync ( callback: callback, with: . failure( error) )
111
+ } else {
112
+ /// The token is valid and not expired.
113
+ Auth . wrapMainAsync ( callback: callback, with: . success( token. token) )
116
114
}
115
+ /// Exit here as this path is for rGCIPFirebaseToken only.
116
+ return
117
+ }
118
+ /// Fallback to standard `currentUser` logic if not in token-only mode.
119
+ if !self . autoRefreshTokens {
120
+ AuthLog . logInfo ( code: " I-AUT000002 " , message: " Token auto-refresh enabled. " )
121
+ self . autoRefreshTokens = true
122
+ self . scheduleAutoTokenRefresh ( )
123
+
124
+ #if os(iOS) || os(tvOS)
125
+ self . applicationDidBecomeActiveObserver =
126
+ NotificationCenter . default. addObserver (
127
+ forName: UIApplication . didBecomeActiveNotification,
128
+ object: nil ,
129
+ queue: nil
130
+ ) { [ weak self] _ in
131
+ guard let self = self , !self . isAppInBackground,
132
+ !self . autoRefreshScheduled else { return }
133
+ self . scheduleAutoTokenRefresh ( )
134
+ }
135
+ self . applicationDidEnterBackgroundObserver =
136
+ NotificationCenter . default. addObserver (
137
+ forName: UIApplication . didEnterBackgroundNotification,
138
+ object: nil ,
139
+ queue: nil
140
+ ) { [ weak self] _ in
141
+ self ? . isAppInBackground = true
142
+ }
143
+ #endif
117
144
}
118
- // Call back with 'nil' if there is no current user.
119
- guard let strongSelf = self , let currentUser = strongSelf . _currentUser else {
145
+
146
+ guard let currentUser = self . _currentUser else {
120
147
DispatchQueue . main. async {
121
148
callback ( nil , nil )
122
149
}
123
150
return
124
151
}
125
152
// Call back with current user token.
126
153
currentUser
127
- . internalGetToken ( forceRefresh: forceRefresh, backend: strongSelf . backend) { token, error in
154
+ . internalGetToken ( forceRefresh: forceRefresh, backend: self . backend) { token, error in
128
155
DispatchQueue . main. async {
129
156
callback ( token, error)
130
157
}
@@ -1645,7 +1672,7 @@ extension Auth: AuthInterop {
1645
1672
init ( app: FirebaseApp ,
1646
1673
keychainStorageProvider: AuthKeychainStorage = AuthKeychainStorageReal . shared,
1647
1674
backend: AuthBackend = . init( rpcIssuer: AuthBackendRPCIssuer ( ) ) ,
1648
- authDispatcher: AuthDispatcher = . init( ) ) {
1675
+ authDispatcher: AuthDispatcher = . init( ) , tenantConfig : TenantConfig ? = nil ) {
1649
1676
self . app = app
1650
1677
mainBundleUrlTypes = Bundle . main
1651
1678
. object ( forInfoDictionaryKey: " CFBundleURLTypes " ) as? [ [ String : Any ] ]
@@ -1668,7 +1695,8 @@ extension Auth: AuthInterop {
1668
1695
appID: app. options. googleAppID,
1669
1696
auth: nil ,
1670
1697
heartbeatLogger: app. heartbeatLogger,
1671
- appCheck: appCheck)
1698
+ appCheck: appCheck,
1699
+ tenantConfig: tenantConfig)
1672
1700
self . backend = backend
1673
1701
self . authDispatcher = authDispatcher
1674
1702
@@ -2264,6 +2292,11 @@ extension Auth: AuthInterop {
2264
2292
return { result in
2265
2293
switch result {
2266
2294
case let . success( authResult) :
2295
+ /// When a standard user successfully signs in, any existing token-only session must be
2296
+ /// invalidated to prevent a conflicting auth state.
2297
+ /// Clear any R-GCIP session state when a standard user signs in. This ensures we exit
2298
+ /// Token-Only Mode.
2299
+ self . rGCIPFirebaseTokenLock. withLock { $0 = nil }
2267
2300
do {
2268
2301
try self . updateCurrentUser ( authResult. user, byForce: false , savingToDisk: true )
2269
2302
Auth . wrapMainAsync ( callback: callback, with: . success( authResult) )
@@ -2427,4 +2460,115 @@ extension Auth: AuthInterop {
2427
2460
///
2428
2461
/// Mutations should occur within a @synchronized(self) context.
2429
2462
private var listenerHandles : NSMutableArray = [ ]
2463
+
2464
+ // R-GCIP Token-Only Session State
2465
+
2466
+ /// The session token obtained from a successful `exchangeToken` call, protected by a lock.
2467
+ ///
2468
+ /// This property is used to support a "token-only" authentication mode for Regionalized
2469
+ /// GCIP, where no `User` object is created. It is mutually exclusive with `_currentUser`.
2470
+ /// If the wrapped value is non-nil, the `AuthInterop` layer will use it for token generation
2471
+ /// instead of relying on a `currentUser`.
2472
+ private let rGCIPFirebaseTokenLock = FIRAllocatedUnfairLock < FirebaseToken ? > ( initialState: nil )
2473
+ }
2474
+
2475
+ // MARK: - Regionalized Auth
2476
+
2477
+ /// Holds configuration for a Regional Google Cloud Identity Platform (R-GCIP) tenant.
2478
+ public struct TenantConfig : Sendable {
2479
+ public let tenantId : String
2480
+ public let location : String
2481
+ /// Initializes a `TenantConfig` instance.
2482
+ ///
2483
+ /// - Parameters:
2484
+ /// - tenantId: The ID of the tenant.
2485
+ /// - location: The location of the tenant. Defaults to "prod-global".
2486
+ public init ( tenantId: String , location: String = " prod-global " ) {
2487
+ self . location = location
2488
+ self . tenantId = tenantId
2489
+ }
2490
+ }
2491
+
2492
+ /// Represents the result of a successful OIDC token exchange, containing a Firebase ID token
2493
+ /// and its expiration.
2494
+ public struct FirebaseToken : Sendable {
2495
+ /// The Firebase ID token string.
2496
+ public let token : String
2497
+ /// The date at which the Firebase ID token expires.
2498
+ public let expirationDate : Date
2499
+
2500
+ init ( token: String , expirationDate: Date ) {
2501
+ self . token = token
2502
+ self . expirationDate = expirationDate
2503
+ }
2504
+ }
2505
+
2506
+ /// Regionalized auth
2507
+ @available ( iOS 13 , tvOS 13 , macOS 10 . 15 , macCatalyst 13 , watchOS 7 , * )
2508
+ public extension Auth {
2509
+ /// Gets the Auth object for a `FirebaseApp` configured for a specific Regional Google Cloud
2510
+ /// Identity Platform (R-GCIP) tenant.
2511
+ ///
2512
+ /// Use this method to create an `Auth` instance that interacts with a regionalized
2513
+ /// authentication backend instead of the default endpoint.
2514
+ ///
2515
+ /// - Parameters:
2516
+ /// - app: The Firebase app instance.
2517
+ /// - tenantConfig: The configuration for the R-GCIP tenant, specifying the tenant ID and its
2518
+ /// location.
2519
+ /// - Returns: The `Auth` instance associated with the given app and tenant config.
2520
+ static func auth( app: FirebaseApp , tenantConfig: TenantConfig ) -> Auth {
2521
+ return Auth ( app: app, tenantConfig: tenantConfig)
2522
+ }
2523
+
2524
+ /// Exchanges a third-party OIDC ID token for a Firebase ID token.
2525
+ ///
2526
+ /// This method is used for Bring Your Own CIAM (BYO-CIAM) in Regionalized GCIP (R-GCIP),
2527
+ /// where the `Auth` instance must be configured with a `TenantConfig`, including `location`
2528
+ /// and `tenantId`, typically by using `Auth.auth(app:tenantConfig:)`.
2529
+ ///
2530
+ /// Unlike standard sign-in methods, this flow *does not* create or update a `User`object and
2531
+ /// *does not* set `CurrentUser` on the `Auth` instance. It only returns a Firebase token.
2532
+ ///
2533
+ /// - Parameters:
2534
+ /// - oidcToken: The OIDC ID token obtained from the third-party identity provider.
2535
+ /// - idpConfigId: The ID of the Identity Provider configuration within your GCIP tenant
2536
+ /// - useStaging: A Boolean value indicating whether to use the staging Identity Platform
2537
+ /// backend. Defaults to `false`.
2538
+ /// - Returns: A `FirebaseToken` containing the Firebase ID token and its expiration date.
2539
+ /// - Throws: An error if the `Auth` instance is not configured for R-GCIP, if the network
2540
+ /// call fails, or if the token response parsing fails.
2541
+ func exchangeToken( idToken: String , idpConfigId: String ,
2542
+ useStaging: Bool = false ) async throws -> FirebaseToken {
2543
+ // Ensure R-GCIP is configured with location and tenant ID
2544
+ guard let _ = requestConfiguration. tenantConfig? . location,
2545
+ let _ = requestConfiguration. tenantConfig? . tenantId
2546
+ else {
2547
+ /// This should never happen in production code, as it indicates a misconfiguration.
2548
+ fatalError ( " R-GCIP is not configured correctly. " )
2549
+ }
2550
+ let request = ExchangeTokenRequest (
2551
+ idToken: idToken,
2552
+ idpConfigID: idpConfigId,
2553
+ config: requestConfiguration,
2554
+ useStaging: useStaging
2555
+ )
2556
+ do {
2557
+ let response = try await backend. call ( with: request)
2558
+ let newToken = FirebaseToken (
2559
+ token: response. firebaseToken,
2560
+ expirationDate: response. expirationDate
2561
+ )
2562
+ // Lock and update the token, signing out any current user.
2563
+ rGCIPFirebaseTokenLock. withLock { token in
2564
+ if self . _currentUser != nil {
2565
+ try ? self . signOut ( )
2566
+ }
2567
+ token = newToken
2568
+ }
2569
+ return newToken
2570
+ } catch {
2571
+ throw error
2572
+ }
2573
+ }
2430
2574
}
0 commit comments