From 860fb465c4e0c36486598e528ff7857e4f873096 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Mon, 19 May 2025 14:25:11 +0530 Subject: [PATCH 01/12] implementing regionalized auth and exchangeToken --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 80 +++++++++++++ .../Backend/AuthRequestConfiguration.swift | 11 +- .../Backend/IdentityToolkitRequest.swift | 112 ++++++++++-------- .../Backend/RPC/ExchangeTokenRequest.swift | 82 +++++++++++++ .../Backend/RPC/ExchangeTokenResponse.swift | 27 +++++ 5 files changed, 263 insertions(+), 49 deletions(-) create mode 100644 FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift create mode 100644 FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 5d8050cc891..d3685421a0b 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -170,6 +170,49 @@ extension Auth: AuthInterop { /// Gets the `FirebaseApp` object that this auth object is connected to. @objc public internal(set) weak var app: FirebaseApp? + /// New R-GCIP v2 + BYO-CIAM initializer. + /// + /// This initializer allows to create an `Auth` instance with a specific `tenantConfig` for + /// R-GCIP. + /// - Parameters: + /// - app: The `FirebaseApp` for which to initialize the `Auth` instance. + /// - tenantConfig: The configuration for the tenant, including location and tenant ID. + /// - Returns: An `Auth` instance configured for the specified tenant. + public static func auth(app: FirebaseApp, tenantConfig: TenantConfig) -> Auth { + // start from the legacy initializer so we get a fully-formed Auth object + let auth = auth(app: app) + kAuthGlobalWorkQueue.sync { + auth.requestConfiguration.location = tenantConfig.location + auth.requestConfiguration.tenantId = tenantConfig.tenantId + } + return auth + } + + /// Holds configuration for a R-GCIP tenant. + public struct TenantConfig { + public let location: String /// The location of the tenant. + public let tenantId: String /// The ID of the tenant. + + /// Initializes a `TenantConfig` instance. + /// - Parameters: + /// - location: The location of the tenant, defaults to "prod-global". + /// - tenantId: The ID of the tenant. + public init(location: String = "prod-global", tenantId: String) { + self.location = location + self.tenantId = tenantId + } + } + + /// Holds a Firebase ID token and its expiration. + public struct AuthExchangeToken { + public let token: String + public let expirationDate: Date? + init(token: String, expirationDate: Date?) { + self.token = token + self.expirationDate = expirationDate + } + } + /// Synchronously gets the cached current user, or null if there is none. @objc public var currentUser: User? { kAuthGlobalWorkQueue.sync { @@ -2425,3 +2468,40 @@ extension Auth: AuthInterop { /// Mutations should occur within a @synchronized(self) context. private var listenerHandles: NSMutableArray = [] } + +@available(iOS 13, *) +public extension Auth { + /// Exchanges a third-party OIDC token for a Firebase STS token. + /// + /// Requires the `Auth` instance to be configured with a `TenantConfig` for R-GCIP. + /// - Parameters: + /// - idpConfigID: The ID of the OIDC provider configuration. + /// - ciamOidcToken: The OIDC token to exchange. + /// - completion: Called with the Firebase ID token or an error. + @objc func exchangeToken(_ idpConfigID: String, + _ ciamOidcToken: String, + completion: @escaping (String?, Error?) -> Void) { + // Check if R-GCIP (location and tenantId) is configured. + guard let location = requestConfiguration.location, + let tenantId = requestConfiguration.tenantId + else { + completion(nil, AuthErrorUtils.operationNotAllowedError( + message: "Set location & tenantId first" + )) + return + } + let request = ExchangeTokenRequest( + idpConfigID: idpConfigID, + idToken: ciamOidcToken, + config: requestConfiguration + ) + Task { + do { + let resp = try await backend.call(with: request) + DispatchQueue.main.async { completion(resp.firebaseToken, nil) } + } catch { + DispatchQueue.main.async { completion(nil, error) } + } + } + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift index 91f99c266f8..deba4e985c1 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -43,16 +43,25 @@ final class AuthRequestConfiguration { /// If set, the local emulator host and port to point to instead of the remote backend. var emulatorHostAndPort: String? + + /// R-GCIP region, set once during Auth init. + var location: String? + + /// R-GCIP tenantId, set once during Auth init. + var tenantId: String? init(apiKey: String, appID: String, auth: Auth? = nil, heartbeatLogger: FIRHeartbeatLoggerProtocol? = nil, - appCheck: AppCheckInterop? = nil) { + appCheck: AppCheckInterop? = nil, + tenantConfig: Auth.TenantConfig? = nil) { self.apiKey = apiKey self.appID = appID self.auth = auth self.heartbeatLogger = heartbeatLogger self.appCheck = appCheck + location = tenantConfig?.location + tenantId = tenantConfig?.tenantId } } diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index 7378b517a7d..8028cf0c777 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -16,43 +16,40 @@ import Foundation private let kHttpsProtocol = "https:" private let kHttpProtocol = "http:" - -private let kEmulatorHostAndPrefixFormat = "%@/%@" - -/// Host for server API calls. This should be changed via -/// `IdentityToolkitRequest.setHost(_ host:)` for testing purposes only. -private nonisolated(unsafe) var gAPIHost = "www.googleapis.com" - +// Legacy GCIP v1 hosts private let kFirebaseAuthAPIHost = "www.googleapis.com" -private let kIdentityPlatformAPIHost = "identitytoolkit.googleapis.com" - private let kFirebaseAuthStagingAPIHost = "staging-www.sandbox.googleapis.com" -private let kIdentityPlatformStagingAPIHost = - "staging-identitytoolkit.sandbox.googleapis.com" - -/// Represents a request to an identity toolkit endpoint. +// Regional R-GCIP v2 hosts +private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com" +private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com" +#if compiler(>=6) + private nonisolated(unsafe) var gAPIHost = "www.googleapis.com" +#else + private var gAPIHost = "www.googleapis.com" +#endif +/// Represents a request to an Identity Toolkit endpoint, routing either to +/// legacy GCIP v1 or regionalized R-GCIP v2 based on presence of tenantID. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class IdentityToolkitRequest { - /// Gets the RPC's endpoint. + /// RPC endpoint name, e.g. "signInWithPassword" or full exchange path let endpoint: String - /// Gets the client's API key used for the request. var apiKey: String - /// The tenant ID of the request. nil if none is available. let tenantID: String? - /// The toggle of using Identity Platform endpoints. let useIdentityPlatform: Bool - /// The toggle of using staging endpoints. let useStaging: Bool - /// The type of the client that the request sent from, which should be CLIENT_TYPE_IOS; var clientType: String - private let _requestConfiguration: AuthRequestConfiguration + /// Optional local emulator host and port + var emulatorHostAndPort: String? { + return _requestConfiguration.emulatorHostAndPort + } + private let _requestConfiguration: AuthRequestConfiguration init(endpoint: String, requestConfiguration: AuthRequestConfiguration, useIdentityPlatform: Bool = false, useStaging: Bool = false) { self.endpoint = endpoint @@ -64,42 +61,61 @@ class IdentityToolkitRequest { tenantID = requestConfiguration.auth?.tenantID } + /// Override this if you need query parameters (default none) func queryParams() -> String { return "" } - /// Returns the request's full URL. + /// Provide the same configuration for AuthBackend + func requestConfiguration() -> AuthRequestConfiguration { + return _requestConfiguration + } + + /// Build the full URL, branching on whether tenantID is set. func requestURL() -> URL { - let apiProtocol: String - let apiHostAndPathPrefix: String + guard let auth = _requestConfiguration.auth else { + fatalError("Internal Auth error: missing Auth on requestConfiguration") + } + let protocolScheme: String + let hostPrefix: String let urlString: String - let emulatorHostAndPort = _requestConfiguration.emulatorHostAndPort - if useIdentityPlatform { - if let emulatorHostAndPort = emulatorHostAndPort { - apiProtocol = kHttpProtocol - apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kIdentityPlatformAPIHost)" + // R-GCIP v2 if location is non-nil + let tenant = _requestConfiguration.tenantId + if let region = _requestConfiguration.location { + // Project identifier + guard let project = auth.app?.options.projectID else { + fatalError("Internal Auth error: missing projectID") + } + // Choose emulator, staging, or prod host + if let emu = emulatorHostAndPort { + protocolScheme = kHttpProtocol + hostPrefix = "\(emu)/\(kRegionalGCIPAPIHost)" } else if useStaging { - apiHostAndPathPrefix = kIdentityPlatformStagingAPIHost - apiProtocol = kHttpsProtocol + protocolScheme = kHttpsProtocol + hostPrefix = kRegionalGCIPStagingAPIHost } else { - apiHostAndPathPrefix = kIdentityPlatformAPIHost - apiProtocol = kHttpsProtocol + protocolScheme = kHttpsProtocol + hostPrefix = kRegionalGCIPAPIHost } - urlString = "\(apiProtocol)//\(apiHostAndPathPrefix)/v2/\(endpoint)?key=\(apiKey)" - + // Regionalized v2 path + urlString = + "\(protocolScheme)//\(hostPrefix)/v2/projects/\(project)" + + "/locations/\(region)/tenants/\(tenant)/idpConfigs/\(endpoint)?key=\(apiKey)" } else { - if let emulatorHostAndPort = emulatorHostAndPort { - apiProtocol = kHttpProtocol - apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kFirebaseAuthAPIHost)" + // Legacy GCIP v1 branch + if let emu = emulatorHostAndPort { + protocolScheme = kHttpProtocol + hostPrefix = "\(emu)/\(kFirebaseAuthAPIHost)" } else if useStaging { - apiProtocol = kHttpsProtocol - apiHostAndPathPrefix = kFirebaseAuthStagingAPIHost + protocolScheme = kHttpsProtocol + hostPrefix = kFirebaseAuthStagingAPIHost } else { - apiProtocol = kHttpsProtocol - apiHostAndPathPrefix = kFirebaseAuthAPIHost + protocolScheme = kHttpsProtocol + hostPrefix = kFirebaseAuthAPIHost } urlString = - "\(apiProtocol)//\(apiHostAndPathPrefix)/identitytoolkit/v3/relyingparty/\(endpoint)?key=\(apiKey)" + "\(protocolScheme)//\(hostPrefix)" + + "/identitytoolkit/v3/relyingparty/\(endpoint)?key=\(apiKey)" } guard let returnURL = URL(string: "\(urlString)\(queryParams())") else { fatalError("Internal Auth error: Failed to generate URL for \(urlString)") @@ -107,14 +123,14 @@ class IdentityToolkitRequest { return returnURL } - /// Returns the request's configuration. - func requestConfiguration() -> AuthRequestConfiguration { - _requestConfiguration - } + // MARK: - Testing API - // MARK: Internal API for development + /// For testing: override the global host for legacy flows + static var host: String { + get { gAPIHost } + set { gAPIHost = newValue } + } - static var host: String { gAPIHost } static func setHost(_ host: String) { gAPIHost = host } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift new file mode 100644 index 00000000000..05eca8afbd2 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -0,0 +1,82 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// A request to exchange a third-party OIDC token for a Firebase STS token. +/// +/// This structure encapsulates the parameters required to make an API request +/// to exchange an OIDC token for a Firebase ID token. It conforms to the +/// `AuthRPCRequest` protocol, providing the necessary properties and +/// methods for the authentication backend to perform the request. +@available(iOS 13, *) +struct ExchangeTokenRequest: AuthRPCRequest { + /// The type of the expected response. + typealias Response = ExchangeTokenResponse + + /// The identifier of the OIDC provider configuration. + private let idpConfigID: String + + /// The third-party OIDC token to exchange. + private let idToken: String + + /// The configuration for the request, holding API key, tenant, etc. + private let config: AuthRequestConfiguration + + /// Initializes a new `ExchangeTokenRequest` instance. + /// + /// - Parameters: + /// - idpConfigID: The identifier of the OIDC provider configuration. + /// - idToken: The third-party OIDC token to exchange. + /// - config: The configuration for the request. + init(idpConfigID: String, + idToken: String, + config: AuthRequestConfiguration) { + self.idpConfigID = idpConfigID + self.idToken = idToken + self.config = config + } + + /// The unencoded HTTP request body for the API. + var unencodedHTTPRequestBody: [String: AnyHashable]? { + return ["id_token": idToken] + } + + /// Constructs the URL for the API request. + /// + /// - Returns: The URL for the token exchange endpoint. + /// - FatalError: if location, tenantID, projectID or apiKey are missing. + func requestURL() -> URL { + guard let region = config.location, + let tenant = config.tenantId, + let project = config.auth?.app?.options.projectID + else { + fatalError( + "exchangeOidcToken requires `auth.useIdentityPlatform`, `auth.location`, `auth.tenantID` & `projectID`" + ) + } + let host = "\(region)-identityplatform.googleapis.com" + let path = "/v2/projects/\(project)/locations/\(region)" + + "/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken" + guard let url = URL(string: "https://\(host)\(path)?key=\(config.apiKey)") else { + fatalError("Failed to create URL for exchangeOidcToken") + } + return url + } + + /// Returns the request configuration. + /// + /// - Returns: The `AuthRequestConfiguration`. + func requestConfiguration() -> AuthRequestConfiguration { config } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift new file mode 100644 index 00000000000..d0b15b6c428 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift @@ -0,0 +1,27 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +/// Response containing the new Firebase STS token. +@available(iOS 13, *) +struct ExchangeTokenResponse: AuthRPCResponse { + let firebaseToken: String + init(dictionary: [String: AnyHashable]) throws { + guard let token = dictionary["idToken"] as? String else { + throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) + } + firebaseToken = token + } +} From 0fac248877b352af5c1357efe116dde947b9e6b0 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Wed, 28 May 2025 05:28:20 +0530 Subject: [PATCH 02/12] addressing review comments --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 122 +++++++++++++----- .../Backend/RPC/ExchangeTokenRequest.swift | 10 +- .../Backend/RPC/ExchangeTokenResponse.swift | 31 ++++- 3 files changed, 117 insertions(+), 46 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index d3685421a0b..055267f690a 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -189,25 +189,25 @@ extension Auth: AuthInterop { } /// Holds configuration for a R-GCIP tenant. - public struct TenantConfig { - public let location: String /// The location of the tenant. + public struct TenantConfig: Sendable { public let tenantId: String /// The ID of the tenant. + public let location: String /// The location of the tenant. /// Initializes a `TenantConfig` instance. /// - Parameters: /// - location: The location of the tenant, defaults to "prod-global". /// - tenantId: The ID of the tenant. - public init(location: String = "prod-global", tenantId: String) { + public init(tenantId: String, location: String = "prod-global") { self.location = location self.tenantId = tenantId } } /// Holds a Firebase ID token and its expiration. - public struct AuthExchangeToken { + public struct AuthExchangeToken: Sendable { public let token: String public let expirationDate: Date? - init(token: String, expirationDate: Date?) { + init(token: String, expirationDate: Date) { self.token = token self.expirationDate = expirationDate } @@ -2471,37 +2471,91 @@ extension Auth: AuthInterop { @available(iOS 13, *) public extension Auth { - /// Exchanges a third-party OIDC token for a Firebase STS token. + + /// Exchanges a third-party OIDC token for a Firebase STS token using a completion handler. + /// + /// This method is used in R-GCIP (multi-tenant) environments where the `Auth` instance must + /// be configured with a `TenantConfig`, including `location` and `tenantId`. /// - /// Requires the `Auth` instance to be configured with a `TenantConfig` for R-GCIP. /// - Parameters: - /// - idpConfigID: The ID of the OIDC provider configuration. - /// - ciamOidcToken: The OIDC token to exchange. - /// - completion: Called with the Firebase ID token or an error. - @objc func exchangeToken(_ idpConfigID: String, - _ ciamOidcToken: String, - completion: @escaping (String?, Error?) -> Void) { - // Check if R-GCIP (location and tenantId) is configured. - guard let location = requestConfiguration.location, - let tenantId = requestConfiguration.tenantId - else { - completion(nil, AuthErrorUtils.operationNotAllowedError( - message: "Set location & tenantId first" - )) - return - } - let request = ExchangeTokenRequest( - idpConfigID: idpConfigID, - idToken: ciamOidcToken, - config: requestConfiguration - ) - Task { - do { - let resp = try await backend.call(with: request) - DispatchQueue.main.async { completion(resp.firebaseToken, nil) } - } catch { - DispatchQueue.main.async { completion(nil, error) } + /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). + /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. + /// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`. + public func exchangeToken( + idToken: String, + idpConfigId: String, + completion: @escaping (AuthTokenResult?, Error?) -> Void + ) { + // Ensure R-GCIP is configured with location and tenant ID + guard let location = requestConfiguration.location, + let tenantId = requestConfiguration.tenantId + else { + completion(nil, AuthErrorCode.operationNotAllowed) + return } - } + + // Build the exchange token request + let request = ExchangeTokenRequest( + idToken: idToken, + idpConfigID: idpConfigId, + config: requestConfiguration + ) + + // Perform the token exchange asynchronously + Task { + do { + let response = try await backend.call(with: request) + do { + // Try to parse the Firebase token response + let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) + DispatchQueue.main.async { + completion(authTokenResult, nil) + } + } catch { + // Failed to parse JWT + DispatchQueue.main.async { + completion(nil, AuthErrorCode.malformedJWT) + } + } + } catch { + // Backend call failed + DispatchQueue.main.async { + completion(nil, error) + } + } + } + } + + /// Exchanges a third-party OIDC token for a Firebase STS token using Swift concurrency. + /// + /// This async variant performs the same operation as the completion-based method but returns + /// the result directly and throws on failure. + /// + /// The `Auth` instance must be configured with `TenantConfig` containing `location` and `tenantId`. + /// + /// - Parameters: + /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). + /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. + /// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details. + /// - Throws: An error if R-GCIP is not configured, if the network call fails, + /// or if the token parsing fails. + public func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult { + // Ensure R-GCIP is configured with location and tenant ID + guard let location = requestConfiguration.location, + let tenantId = requestConfiguration.tenantId + else { + throw AuthErrorCode.operationNotAllowed + } + + // Build the exchange token request + let request = ExchangeTokenRequest( + idToken: idToken, + idpConfigID: idpConfigId, + config: requestConfiguration + ) + + // Perform the backend call and return parsed token + let response = try await backend.call(with: request) + return try AuthTokenResult.tokenResult(token: response.firebaseToken) } } diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift index 05eca8afbd2..0a3b4e6a672 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -25,11 +25,11 @@ struct ExchangeTokenRequest: AuthRPCRequest { /// The type of the expected response. typealias Response = ExchangeTokenResponse - /// The identifier of the OIDC provider configuration. - private let idpConfigID: String - /// The third-party OIDC token to exchange. private let idToken: String + + /// The identifier of the OIDC provider configuration. + private let idpConfigID: String /// The configuration for the request, holding API key, tenant, etc. private let config: AuthRequestConfiguration @@ -40,8 +40,8 @@ struct ExchangeTokenRequest: AuthRPCRequest { /// - idpConfigID: The identifier of the OIDC provider configuration. /// - idToken: The third-party OIDC token to exchange. /// - config: The configuration for the request. - init(idpConfigID: String, - idToken: String, + init(idToken: String, + idpConfigID: String, config: AuthRequestConfiguration) { self.idpConfigID = idpConfigID self.idToken = idToken diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift index d0b15b6c428..50e21335be9 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift @@ -14,14 +14,31 @@ import Foundation -/// Response containing the new Firebase STS token. +import Foundation + +/// Response containing the new Firebase STS token and its expiration time in seconds. @available(iOS 13, *) struct ExchangeTokenResponse: AuthRPCResponse { - let firebaseToken: String - init(dictionary: [String: AnyHashable]) throws { - guard let token = dictionary["idToken"] as? String else { - throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) + /// The Firebase ID token. + let firebaseToken: String + + /// The time interval (in *seconds*) until the token expires. + let expiresIn: TimeInterval + + /// The expiration date of the token, calculated from `expiresInSeconds`. + let expirationDate: Date + + /// Initializes a new ExchangeTokenResponse from a dictionary. + /// + /// - Parameter dictionary: The dictionary representing the JSON response from the server. + /// - Throws: `AuthErrorUtils.unexpectedResponse` if the dictionary is missing required fields + /// or contains invalid data. + init(dictionary: [String: AnyHashable]) throws { + guard let token = dictionary["idToken"] as? String else { + throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) + } + self.firebaseToken = token + expiresIn = (dictionary["expiresIn"] as? TimeInterval) ?? 3600 + expirationDate = Date().addingTimeInterval(expiresIn) } - firebaseToken = token - } } From 5bf916260cff0f2b43392e96cb4c0c17d156aa1d Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Wed, 28 May 2025 06:14:19 +0530 Subject: [PATCH 03/12] lint fixes --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 184 +++++++++--------- .../Backend/AuthRequestConfiguration.swift | 2 +- .../Backend/RPC/ExchangeTokenRequest.swift | 2 +- .../Backend/RPC/ExchangeTokenResponse.swift | 36 ++-- 4 files changed, 111 insertions(+), 113 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 055267f690a..cef52cb3ed9 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -171,48 +171,48 @@ extension Auth: AuthInterop { @objc public internal(set) weak var app: FirebaseApp? /// New R-GCIP v2 + BYO-CIAM initializer. - /// - /// This initializer allows to create an `Auth` instance with a specific `tenantConfig` for - /// R-GCIP. - /// - Parameters: - /// - app: The `FirebaseApp` for which to initialize the `Auth` instance. - /// - tenantConfig: The configuration for the tenant, including location and tenant ID. - /// - Returns: An `Auth` instance configured for the specified tenant. - public static func auth(app: FirebaseApp, tenantConfig: TenantConfig) -> Auth { - // start from the legacy initializer so we get a fully-formed Auth object - let auth = auth(app: app) - kAuthGlobalWorkQueue.sync { - auth.requestConfiguration.location = tenantConfig.location - auth.requestConfiguration.tenantId = tenantConfig.tenantId - } - return auth + /// + /// This initializer allows to create an `Auth` instance with a specific `tenantConfig` for + /// R-GCIP. + /// - Parameters: + /// - app: The `FirebaseApp` for which to initialize the `Auth` instance. + /// - tenantConfig: The configuration for the tenant, including location and tenant ID. + /// - Returns: An `Auth` instance configured for the specified tenant. + public static func auth(app: FirebaseApp, tenantConfig: TenantConfig) -> Auth { + // start from the legacy initializer so we get a fully-formed Auth object + let auth = auth(app: app) + kAuthGlobalWorkQueue.sync { + auth.requestConfiguration.location = tenantConfig.location + auth.requestConfiguration.tenantId = tenantConfig.tenantId } + return auth + } - /// Holds configuration for a R-GCIP tenant. + /// Holds configuration for a R-GCIP tenant. public struct TenantConfig: Sendable { - public let tenantId: String /// The ID of the tenant. - public let location: String /// The location of the tenant. + public let tenantId: String /// The ID of the tenant. + public let location: String /// The location of the tenant. - /// Initializes a `TenantConfig` instance. - /// - Parameters: - /// - location: The location of the tenant, defaults to "prod-global". - /// - tenantId: The ID of the tenant. - public init(tenantId: String, location: String = "prod-global") { - self.location = location - self.tenantId = tenantId - } + /// Initializes a `TenantConfig` instance. + /// - Parameters: + /// - location: The location of the tenant, defaults to "prod-global". + /// - tenantId: The ID of the tenant. + public init(tenantId: String, location: String = "prod-global") { + self.location = location + self.tenantId = tenantId } + } - /// Holds a Firebase ID token and its expiration. + /// Holds a Firebase ID token and its expiration. public struct AuthExchangeToken: Sendable { - public let token: String - public let expirationDate: Date? - init(token: String, expirationDate: Date) { - self.token = token - self.expirationDate = expirationDate - } + public let token: String + public let expirationDate: Date? + init(token: String, expirationDate: Date) { + self.token = token + self.expirationDate = expirationDate } - + } + /// Synchronously gets the cached current user, or null if there is none. @objc public var currentUser: User? { kAuthGlobalWorkQueue.sync { @@ -2471,7 +2471,6 @@ extension Auth: AuthInterop { @available(iOS 13, *) public extension Auth { - /// Exchanges a third-party OIDC token for a Firebase STS token using a completion handler. /// /// This method is used in R-GCIP (multi-tenant) environments where the `Auth` instance must @@ -2481,57 +2480,56 @@ public extension Auth { /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. /// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`. - public func exchangeToken( - idToken: String, - idpConfigId: String, - completion: @escaping (AuthTokenResult?, Error?) -> Void - ) { - // Ensure R-GCIP is configured with location and tenant ID - guard let location = requestConfiguration.location, - let tenantId = requestConfiguration.tenantId - else { - completion(nil, AuthErrorCode.operationNotAllowed) - return - } + func exchangeToken(idToken: String, + idpConfigId: String, + completion: @escaping (AuthTokenResult?, Error?) -> Void) { + // Ensure R-GCIP is configured with location and tenant ID + guard let location = requestConfiguration.location, + let tenantId = requestConfiguration.tenantId + else { + completion(nil, AuthErrorCode.operationNotAllowed) + return + } - // Build the exchange token request - let request = ExchangeTokenRequest( - idToken: idToken, - idpConfigID: idpConfigId, - config: requestConfiguration - ) + // Build the exchange token request + let request = ExchangeTokenRequest( + idToken: idToken, + idpConfigID: idpConfigId, + config: requestConfiguration + ) - // Perform the token exchange asynchronously - Task { - do { - let response = try await backend.call(with: request) - do { - // Try to parse the Firebase token response - let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) - DispatchQueue.main.async { - completion(authTokenResult, nil) - } - } catch { - // Failed to parse JWT - DispatchQueue.main.async { - completion(nil, AuthErrorCode.malformedJWT) - } - } - } catch { - // Backend call failed - DispatchQueue.main.async { - completion(nil, error) - } + // Perform the token exchange asynchronously + Task { + do { + let response = try await backend.call(with: request) + do { + // Try to parse the Firebase token response + let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) + DispatchQueue.main.async { + completion(authTokenResult, nil) + } + } catch { + // Failed to parse JWT + DispatchQueue.main.async { + completion(nil, AuthErrorCode.malformedJWT) } + } + } catch { + // Backend call failed + DispatchQueue.main.async { + completion(nil, error) + } } + } } - + /// Exchanges a third-party OIDC token for a Firebase STS token using Swift concurrency. /// /// This async variant performs the same operation as the completion-based method but returns /// the result directly and throws on failure. /// - /// The `Auth` instance must be configured with `TenantConfig` containing `location` and `tenantId`. + /// The `Auth` instance must be configured with `TenantConfig` containing `location` and + /// `tenantId`. /// /// - Parameters: /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). @@ -2539,23 +2537,23 @@ public extension Auth { /// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details. /// - Throws: An error if R-GCIP is not configured, if the network call fails, /// or if the token parsing fails. - public func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult { - // Ensure R-GCIP is configured with location and tenant ID - guard let location = requestConfiguration.location, - let tenantId = requestConfiguration.tenantId - else { - throw AuthErrorCode.operationNotAllowed - } - - // Build the exchange token request - let request = ExchangeTokenRequest( - idToken: idToken, - idpConfigID: idpConfigId, - config: requestConfiguration - ) + func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult { + // Ensure R-GCIP is configured with location and tenant ID + guard let location = requestConfiguration.location, + let tenantId = requestConfiguration.tenantId + else { + throw AuthErrorCode.operationNotAllowed + } + + // Build the exchange token request + let request = ExchangeTokenRequest( + idToken: idToken, + idpConfigID: idpConfigId, + config: requestConfiguration + ) - // Perform the backend call and return parsed token - let response = try await backend.call(with: request) - return try AuthTokenResult.tokenResult(token: response.firebaseToken) + // Perform the backend call and return parsed token + let response = try await backend.call(with: request) + return try AuthTokenResult.tokenResult(token: response.firebaseToken) } } diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift index deba4e985c1..95d4f3fdc89 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -43,7 +43,7 @@ final class AuthRequestConfiguration { /// If set, the local emulator host and port to point to instead of the remote backend. var emulatorHostAndPort: String? - + /// R-GCIP region, set once during Auth init. var location: String? diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift index 0a3b4e6a672..712a5a43d67 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -27,7 +27,7 @@ struct ExchangeTokenRequest: AuthRPCRequest { /// The third-party OIDC token to exchange. private let idToken: String - + /// The identifier of the OIDC provider configuration. private let idpConfigID: String diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift index 50e21335be9..d730d66ab50 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift @@ -19,26 +19,26 @@ import Foundation /// Response containing the new Firebase STS token and its expiration time in seconds. @available(iOS 13, *) struct ExchangeTokenResponse: AuthRPCResponse { - /// The Firebase ID token. - let firebaseToken: String + /// The Firebase ID token. + let firebaseToken: String - /// The time interval (in *seconds*) until the token expires. - let expiresIn: TimeInterval + /// The time interval (in *seconds*) until the token expires. + let expiresIn: TimeInterval - /// The expiration date of the token, calculated from `expiresInSeconds`. - let expirationDate: Date + /// The expiration date of the token, calculated from `expiresInSeconds`. + let expirationDate: Date - /// Initializes a new ExchangeTokenResponse from a dictionary. - /// - /// - Parameter dictionary: The dictionary representing the JSON response from the server. - /// - Throws: `AuthErrorUtils.unexpectedResponse` if the dictionary is missing required fields - /// or contains invalid data. - init(dictionary: [String: AnyHashable]) throws { - guard let token = dictionary["idToken"] as? String else { - throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) - } - self.firebaseToken = token - expiresIn = (dictionary["expiresIn"] as? TimeInterval) ?? 3600 - expirationDate = Date().addingTimeInterval(expiresIn) + /// Initializes a new ExchangeTokenResponse from a dictionary. + /// + /// - Parameter dictionary: The dictionary representing the JSON response from the server. + /// - Throws: `AuthErrorUtils.unexpectedResponse` if the dictionary is missing required fields + /// or contains invalid data. + init(dictionary: [String: AnyHashable]) throws { + guard let token = dictionary["idToken"] as? String else { + throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) } + firebaseToken = token + expiresIn = (dictionary["expiresIn"] as? TimeInterval) ?? 3600 + expirationDate = Date().addingTimeInterval(expiresIn) + } } From 60940b7b0bd52d9b6a30bc9587eb6e3545e3b0f1 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Wed, 28 May 2025 06:31:27 +0530 Subject: [PATCH 04/12] error fixing --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 32 +++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index cef52cb3ed9..b7dde6c194c 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2484,8 +2484,8 @@ public extension Auth { idpConfigId: String, completion: @escaping (AuthTokenResult?, Error?) -> Void) { // Ensure R-GCIP is configured with location and tenant ID - guard let location = requestConfiguration.location, - let tenantId = requestConfiguration.tenantId + guard let _ = requestConfiguration.location, + let _ = requestConfiguration.tenantId else { completion(nil, AuthErrorCode.operationNotAllowed) return @@ -2504,19 +2504,15 @@ public extension Auth { let response = try await backend.call(with: request) do { // Try to parse the Firebase token response - let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) - DispatchQueue.main.async { + do { + let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) + completion(authTokenResult, nil) completion(authTokenResult, nil) } } catch { - // Failed to parse JWT - DispatchQueue.main.async { - completion(nil, AuthErrorCode.malformedJWT) - } - } - } catch { - // Backend call failed - DispatchQueue.main.async { + completion(nil, AuthErrorCode.malformedJWT) + } catch { + // 5. Handle other errors (network, server errors, etc.). completion(nil, error) } } @@ -2539,8 +2535,8 @@ public extension Auth { /// or if the token parsing fails. func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult { // Ensure R-GCIP is configured with location and tenant ID - guard let location = requestConfiguration.location, - let tenantId = requestConfiguration.tenantId + guard let _ = requestConfiguration.location, + let _ = requestConfiguration.tenantId else { throw AuthErrorCode.operationNotAllowed } @@ -2553,7 +2549,11 @@ public extension Auth { ) // Perform the backend call and return parsed token - let response = try await backend.call(with: request) - return try AuthTokenResult.tokenResult(token: response.firebaseToken) + do { + let response = try await backend.call(with: request) + return try AuthTokenResult.tokenResult(token: response.firebaseToken) + } catch { + throw error + } } } From dcdfa6609df571bcabf307bd893b697a07fb755e Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Wed, 28 May 2025 07:35:54 +0530 Subject: [PATCH 05/12] resolving git checks --- .../Sources/Swift/Backend/IdentityToolkitRequest.swift | 9 +++++++-- .../Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift | 8 ++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index 8028cf0c777..56675ec7233 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +import FirebaseAuth // Import FirebaseAuth for AuthErrorCode +import FirebaseCore import Foundation private let kHttpsProtocol = "https:" @@ -80,8 +82,11 @@ class IdentityToolkitRequest { let hostPrefix: String let urlString: String // R-GCIP v2 if location is non-nil - let tenant = _requestConfiguration.tenantId - if let region = _requestConfiguration.location { + + if let region = _requestConfiguration.location, + let tenant = _requestConfiguration.tenantId, + !region.isEmpty, + !tenant.isEmpty { // Project identifier guard let project = auth.app?.options.projectID else { fatalError("Internal Auth error: missing projectID") diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift index 712a5a43d67..bab9187cfae 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -25,10 +25,10 @@ struct ExchangeTokenRequest: AuthRPCRequest { /// The type of the expected response. typealias Response = ExchangeTokenResponse - /// The third-party OIDC token to exchange. + /// The OIDC provider's Authorization code or Id Token to exchange. private let idToken: String - /// The identifier of the OIDC provider configuration. + /// The ExternalUserDirectoryId corresponding to the OIDC custom Token. private let idpConfigID: String /// The configuration for the request, holding API key, tenant, etc. @@ -67,8 +67,8 @@ struct ExchangeTokenRequest: AuthRPCRequest { ) } let host = "\(region)-identityplatform.googleapis.com" - let path = "/v2/projects/\(project)/locations/\(region)" + - "/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken" + let path = "/v2/projects/$\(project)/locations/$\(region)" + + "/tenants/$\(tenant)/idpConfigs/$\(idpConfigID):exchangeOidcToken" guard let url = URL(string: "https://\(host)\(path)?key=\(config.apiKey)") else { fatalError("Failed to create URL for exchangeOidcToken") } From aed4e2e61e1d97f3a07af0b651510c084f888081 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Wed, 28 May 2025 09:03:21 +0530 Subject: [PATCH 06/12] resolving git checks --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 11 ++++------- .../Swift/Backend/IdentityToolkitRequest.swift | 1 - .../Swift/Backend/RPC/ExchangeTokenRequest.swift | 10 ++++++---- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index b7dde6c194c..dc309d93826 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2480,7 +2480,7 @@ public extension Auth { /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. /// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`. - func exchangeToken(idToken: String, + func exchangeToken(customToken: String, idpConfigId: String, completion: @escaping (AuthTokenResult?, Error?) -> Void) { // Ensure R-GCIP is configured with location and tenant ID @@ -2493,7 +2493,7 @@ public extension Auth { // Build the exchange token request let request = ExchangeTokenRequest( - idToken: idToken, + customToken: customToken, idpConfigID: idpConfigId, config: requestConfiguration ) @@ -2511,9 +2511,6 @@ public extension Auth { } } catch { completion(nil, AuthErrorCode.malformedJWT) - } catch { - // 5. Handle other errors (network, server errors, etc.). - completion(nil, error) } } } @@ -2533,7 +2530,7 @@ public extension Auth { /// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details. /// - Throws: An error if R-GCIP is not configured, if the network call fails, /// or if the token parsing fails. - func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult { + func exchangeToken(customToken: String, idpConfigId: String) async throws -> AuthTokenResult { // Ensure R-GCIP is configured with location and tenant ID guard let _ = requestConfiguration.location, let _ = requestConfiguration.tenantId @@ -2543,7 +2540,7 @@ public extension Auth { // Build the exchange token request let request = ExchangeTokenRequest( - idToken: idToken, + customToken: customToken, idpConfigID: idpConfigId, config: requestConfiguration ) diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index 56675ec7233..6f15250afeb 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseAuth // Import FirebaseAuth for AuthErrorCode import FirebaseCore import Foundation diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift index bab9187cfae..fdee3dc1fb9 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -14,6 +14,8 @@ import Foundation +private let kCustomTokenKey = "customToken" + /// A request to exchange a third-party OIDC token for a Firebase STS token. /// /// This structure encapsulates the parameters required to make an API request @@ -26,7 +28,7 @@ struct ExchangeTokenRequest: AuthRPCRequest { typealias Response = ExchangeTokenResponse /// The OIDC provider's Authorization code or Id Token to exchange. - private let idToken: String + private let customToken: String /// The ExternalUserDirectoryId corresponding to the OIDC custom Token. private let idpConfigID: String @@ -40,17 +42,17 @@ struct ExchangeTokenRequest: AuthRPCRequest { /// - idpConfigID: The identifier of the OIDC provider configuration. /// - idToken: The third-party OIDC token to exchange. /// - config: The configuration for the request. - init(idToken: String, + init(customToken: String, idpConfigID: String, config: AuthRequestConfiguration) { self.idpConfigID = idpConfigID - self.idToken = idToken + self.customToken = customToken self.config = config } /// The unencoded HTTP request body for the API. var unencodedHTTPRequestBody: [String: AnyHashable]? { - return ["id_token": idToken] + return ["custom_token": customToken] } /// Constructs the URL for the API request. From d86b0d15594df6c4f7f29f3c6c287cb015553ca6 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Thu, 29 May 2025 23:12:55 +0530 Subject: [PATCH 07/12] updating changes --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 114 ++++++++---------- .../Backend/AuthRequestConfiguration.swift | 2 +- .../Backend/IdentityToolkitRequest.swift | 17 ++- .../Backend/RPC/ExchangeTokenRequest.swift | 20 ++- 4 files changed, 78 insertions(+), 75 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index dc309d93826..a8015b69c5f 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -140,6 +140,31 @@ extension Auth: AuthInterop { } } +/// Holds configuration for a R-GCIP tenant. +public struct TenantConfig: Sendable { + public let tenantId: String /// The ID of the tenant. + public let location: String /// The location of the tenant. + + /// Initializes a `TenantConfig` instance. + /// - Parameters: + /// - location: The location of the tenant, defaults to "prod-global". + /// - tenantId: The ID of the tenant. + public init(tenantId: String, location: String = "prod-global") { + self.location = location + self.tenantId = tenantId + } +} + +/// Holds a Firebase ID token and its expiration. +public struct AuthExchangeToken: Sendable { + public let token: String + public let expirationDate: Date? + init(token: String, expirationDate: Date) { + self.token = token + self.expirationDate = expirationDate + } +} + /// Manages authentication for Firebase apps. /// /// This class is thread-safe. @@ -170,16 +195,12 @@ extension Auth: AuthInterop { /// Gets the `FirebaseApp` object that this auth object is connected to. @objc public internal(set) weak var app: FirebaseApp? - /// New R-GCIP v2 + BYO-CIAM initializer. - /// - /// This initializer allows to create an `Auth` instance with a specific `tenantConfig` for - /// R-GCIP. + /// Gets the auth object for a `FirebaseApp` with an optional `TenantConfig`. /// - Parameters: - /// - app: The `FirebaseApp` for which to initialize the `Auth` instance. - /// - tenantConfig: The configuration for the tenant, including location and tenant ID. - /// - Returns: An `Auth` instance configured for the specified tenant. + /// - app: The Firebase app instance. + /// - tenantConfig: The optional configuration for the RGCIP. + /// - Returns: The `Auth` instance associated with the given app and tenant config. public static func auth(app: FirebaseApp, tenantConfig: TenantConfig) -> Auth { - // start from the legacy initializer so we get a fully-formed Auth object let auth = auth(app: app) kAuthGlobalWorkQueue.sync { auth.requestConfiguration.location = tenantConfig.location @@ -188,31 +209,6 @@ extension Auth: AuthInterop { return auth } - /// Holds configuration for a R-GCIP tenant. - public struct TenantConfig: Sendable { - public let tenantId: String /// The ID of the tenant. - public let location: String /// The location of the tenant. - - /// Initializes a `TenantConfig` instance. - /// - Parameters: - /// - location: The location of the tenant, defaults to "prod-global". - /// - tenantId: The ID of the tenant. - public init(tenantId: String, location: String = "prod-global") { - self.location = location - self.tenantId = tenantId - } - } - - /// Holds a Firebase ID token and its expiration. - public struct AuthExchangeToken: Sendable { - public let token: String - public let expirationDate: Date? - init(token: String, expirationDate: Date) { - self.token = token - self.expirationDate = expirationDate - } - } - /// Synchronously gets the cached current user, or null if there is none. @objc public var currentUser: User? { kAuthGlobalWorkQueue.sync { @@ -2471,47 +2467,44 @@ extension Auth: AuthInterop { @available(iOS 13, *) public extension Auth { - /// Exchanges a third-party OIDC token for a Firebase STS token using a completion handler. + /// Exchanges a third-party OIDC token for a Firebase STS token. /// /// This method is used in R-GCIP (multi-tenant) environments where the `Auth` instance must /// be configured with a `TenantConfig`, including `location` and `tenantId`. + /// Unlike other sign-in methods, this flow *does not* create or update a `User` object. /// /// - Parameters: - /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). - /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. + /// - request: The ExchangeTokenRequest containing the OIDC token and other parameters. /// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`. func exchangeToken(customToken: String, idpConfigId: String, - completion: @escaping (AuthTokenResult?, Error?) -> Void) { + completion: @escaping (AuthExchangeToken?, Error?) -> Void) { // Ensure R-GCIP is configured with location and tenant ID guard let _ = requestConfiguration.location, let _ = requestConfiguration.tenantId else { - completion(nil, AuthErrorCode.operationNotAllowed) + Auth.wrapMainAsync( + callback: completion, + with: .failure(AuthErrorUtils + .operationNotAllowedError(message: "R-GCIP is not configured.")) + ) return } - - // Build the exchange token request let request = ExchangeTokenRequest( customToken: customToken, idpConfigID: idpConfigId, config: requestConfiguration ) - - // Perform the token exchange asynchronously Task { do { let response = try await backend.call(with: request) - do { - // Try to parse the Firebase token response - do { - let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken) - completion(authTokenResult, nil) - completion(authTokenResult, nil) - } - } catch { - completion(nil, AuthErrorCode.malformedJWT) - } + let authExchangeToken = AuthExchangeToken( + token: response.firebaseToken, + expirationDate: response.expirationDate + ) + Auth.wrapMainAsync(callback: completion, with: .success(authExchangeToken)) + } catch { + Auth.wrapMainAsync(callback: completion, with: .failure(error)) } } } @@ -2523,32 +2516,31 @@ public extension Auth { /// /// The `Auth` instance must be configured with `TenantConfig` containing `location` and /// `tenantId`. + /// Unlike other sign-in methods, this flow *does not* create or update a `User` object. /// /// - Parameters: - /// - idToken: The OIDC token received from the third-party Identity Provider (IdP). - /// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase. + /// - request: The ExchangeTokenRequest containing the OIDC token and other parameters. /// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details. /// - Throws: An error if R-GCIP is not configured, if the network call fails, /// or if the token parsing fails. - func exchangeToken(customToken: String, idpConfigId: String) async throws -> AuthTokenResult { + func exchangeToken(customToken: String, idpConfigId: String) async throws -> AuthExchangeToken { // Ensure R-GCIP is configured with location and tenant ID guard let _ = requestConfiguration.location, let _ = requestConfiguration.tenantId else { - throw AuthErrorCode.operationNotAllowed + throw AuthErrorUtils.operationNotAllowedError(message: "R-GCIP is not configured.") } - - // Build the exchange token request let request = ExchangeTokenRequest( customToken: customToken, idpConfigID: idpConfigId, config: requestConfiguration ) - - // Perform the backend call and return parsed token do { let response = try await backend.call(with: request) - return try AuthTokenResult.tokenResult(token: response.firebaseToken) + return AuthExchangeToken( + token: response.firebaseToken, + expirationDate: response.expirationDate + ) } catch { throw error } diff --git a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift index 95d4f3fdc89..b76e17fa1d1 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -55,7 +55,7 @@ final class AuthRequestConfiguration { auth: Auth? = nil, heartbeatLogger: FIRHeartbeatLoggerProtocol? = nil, appCheck: AppCheckInterop? = nil, - tenantConfig: Auth.TenantConfig? = nil) { + tenantConfig: TenantConfig? = nil) { self.apiKey = apiKey self.appID = appID self.auth = auth diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index 6f15250afeb..f962708ea59 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -28,6 +28,7 @@ private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.goog #else private var gAPIHost = "www.googleapis.com" #endif + /// Represents a request to an Identity Toolkit endpoint, routing either to /// legacy GCIP v1 or regionalized R-GCIP v2 based on presence of tenantID. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) @@ -74,22 +75,16 @@ class IdentityToolkitRequest { /// Build the full URL, branching on whether tenantID is set. func requestURL() -> URL { - guard let auth = _requestConfiguration.auth else { - fatalError("Internal Auth error: missing Auth on requestConfiguration") - } let protocolScheme: String let hostPrefix: String let urlString: String - // R-GCIP v2 if location is non-nil + // R-GCIP v2 if location AND tenantID from requestConfiguration are non-nil. if let region = _requestConfiguration.location, - let tenant = _requestConfiguration.tenantId, + let tenant = _requestConfiguration.tenantId, // Use tenantId from requestConfiguration !region.isEmpty, !tenant.isEmpty { - // Project identifier - guard let project = auth.app?.options.projectID else { - fatalError("Internal Auth error: missing projectID") - } + let projectID = _requestConfiguration.auth?.app?.options.projectID // Choose emulator, staging, or prod host if let emu = emulatorHostAndPort { protocolScheme = kHttpProtocol @@ -101,10 +96,12 @@ class IdentityToolkitRequest { protocolScheme = kHttpsProtocol hostPrefix = kRegionalGCIPAPIHost } + // Regionalized v2 path urlString = - "\(protocolScheme)//\(hostPrefix)/v2/projects/\(project)" + "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID)" + "/locations/\(region)/tenants/\(tenant)/idpConfigs/\(endpoint)?key=\(apiKey)" + } else { // Legacy GCIP v1 branch if let emu = emulatorHostAndPort { diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift index fdee3dc1fb9..4da85b50acf 100644 --- a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -28,13 +28,27 @@ struct ExchangeTokenRequest: AuthRPCRequest { typealias Response = ExchangeTokenResponse /// The OIDC provider's Authorization code or Id Token to exchange. - private let customToken: String + let customToken: String /// The ExternalUserDirectoryId corresponding to the OIDC custom Token. - private let idpConfigID: String + let idpConfigID: String /// The configuration for the request, holding API key, tenant, etc. - private let config: AuthRequestConfiguration + let config: AuthRequestConfiguration + + var path: String { + guard let region = config.location, + let tenant = config.tenantId, + let project = config.auth?.app?.options.projectID + else { + fatalError( + "exchangeOidcToken requires `auth.location` & `auth.tenantID`" + ) + } + _ = "\(region)-identityplatform.googleapis.com" + return "/v2alpha/projects/\(project)/locations/\(region)" + + "/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken" + } /// Initializes a new `ExchangeTokenRequest` instance. /// From dc606c6a910ca4e3381941cabe986f83877db59e Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Thu, 29 May 2025 23:28:22 +0530 Subject: [PATCH 08/12] resolving errors --- FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index f962708ea59..306983f622a 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -99,7 +99,7 @@ class IdentityToolkitRequest { // Regionalized v2 path urlString = - "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID)" + "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID ?? "projectID")" + "/locations/\(region)/tenants/\(tenant)/idpConfigs/\(endpoint)?key=\(apiKey)" } else { From ed886f929656947a519427faa1b5e1b47716fbf3 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 30 May 2025 01:17:05 +0530 Subject: [PATCH 09/12] lint changes --- FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index 306983f622a..aa12c860ad1 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -99,7 +99,7 @@ class IdentityToolkitRequest { // Regionalized v2 path urlString = - "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID ?? "projectID")" + "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID ?? "projectID")" + "/locations/\(region)/tenants/\(tenant)/idpConfigs/\(endpoint)?key=\(apiKey)" } else { From f2703e6d8cbc9ed64a00c16433465c881cf12f05 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 30 May 2025 15:35:54 +0530 Subject: [PATCH 10/12] refactoring identityToolkitRequest with corrected logic for r-gcip --- FirebaseAuth/Sources/Swift/Auth/Auth.swift | 5 + .../Backend/IdentityToolkitRequest.swift | 123 ++++++------ .../Unit/IdentityToolkitRequestTests.swift | 183 ++++++++++++++++++ 3 files changed, 254 insertions(+), 57 deletions(-) diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index a8015b69c5f..aa70469bfe1 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -209,6 +209,11 @@ public struct AuthExchangeToken: Sendable { return auth } +// public static func auth(app: FirebaseApp, tenantConfig: nil) -> Auth { +// let auth = auth(app: app) +// return auth +// } + /// Synchronously gets the cached current user, or null if there is none. @objc public var currentUser: User? { kAuthGlobalWorkQueue.sync { diff --git a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift index aa12c860ad1..ce485ef67bc 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -12,46 +12,51 @@ // See the License for the specific language governing permissions and // limitations under the License. -import FirebaseCore import Foundation private let kHttpsProtocol = "https:" private let kHttpProtocol = "http:" -// Legacy GCIP v1 hosts + +private let kEmulatorHostAndPrefixFormat = "%@/%@" + +/// Host for server API calls. This should be changed via +/// `IdentityToolkitRequest.setHost(_ host:)` for testing purposes only. +private nonisolated(unsafe) var gAPIHost = "www.googleapis.com" + private let kFirebaseAuthAPIHost = "www.googleapis.com" +private let kIdentityPlatformAPIHost = "identitytoolkit.googleapis.com" +private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com" // Regional R-GCIP v2 hosts + private let kFirebaseAuthStagingAPIHost = "staging-www.sandbox.googleapis.com" -// Regional R-GCIP v2 hosts -private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com" -private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com" -#if compiler(>=6) - private nonisolated(unsafe) var gAPIHost = "www.googleapis.com" -#else - private var gAPIHost = "www.googleapis.com" -#endif - -/// Represents a request to an Identity Toolkit endpoint, routing either to -/// legacy GCIP v1 or regionalized R-GCIP v2 based on presence of tenantID. +private let kIdentityPlatformStagingAPIHost = + "staging-identitytoolkit.sandbox.googleapis.com" +private let kRegionalGCIPStagingAPIHost = + "staging-identityplatform.sandbox.googleapis.com" // Regional R-GCIP v2 hosts + +/// Represents a request to an identity toolkit endpoint routing either to legacy GCIP or +/// regionalized R-GCIP @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) class IdentityToolkitRequest { - /// RPC endpoint name, e.g. "signInWithPassword" or full exchange path + /// Gets the RPC's endpoint. let endpoint: String + /// Gets the client's API key used for the request. var apiKey: String - /// The tenant ID of the request. nil if none is available. + + /// The tenant ID of the request. nil if none is available (not for r-gcip). let tenantID: String? + /// The toggle of using Identity Platform endpoints. let useIdentityPlatform: Bool + /// The toggle of using staging endpoints. let useStaging: Bool + /// The type of the client that the request sent from, which should be CLIENT_TYPE_IOS; var clientType: String - /// Optional local emulator host and port - var emulatorHostAndPort: String? { - return _requestConfiguration.emulatorHostAndPort - } - private let _requestConfiguration: AuthRequestConfiguration + init(endpoint: String, requestConfiguration: AuthRequestConfiguration, useIdentityPlatform: Bool = false, useStaging: Bool = false) { self.endpoint = endpoint @@ -63,60 +68,64 @@ class IdentityToolkitRequest { tenantID = requestConfiguration.auth?.tenantID } - /// Override this if you need query parameters (default none) func queryParams() -> String { return "" } - /// Provide the same configuration for AuthBackend - func requestConfiguration() -> AuthRequestConfiguration { - return _requestConfiguration - } - - /// Build the full URL, branching on whether tenantID is set. + /// Returns the request's full URL. func requestURL() -> URL { - let protocolScheme: String - let hostPrefix: String + let apiProtocol: String + let apiHostAndPathPrefix: String let urlString: String - - // R-GCIP v2 if location AND tenantID from requestConfiguration are non-nil. + let emulatorHostAndPort = _requestConfiguration.emulatorHostAndPort + /// R-GCIP if let region = _requestConfiguration.location, let tenant = _requestConfiguration.tenantId, // Use tenantId from requestConfiguration !region.isEmpty, !tenant.isEmpty { let projectID = _requestConfiguration.auth?.app?.options.projectID // Choose emulator, staging, or prod host - if let emu = emulatorHostAndPort { - protocolScheme = kHttpProtocol - hostPrefix = "\(emu)/\(kRegionalGCIPAPIHost)" + if let emulatorHostAndPort = emulatorHostAndPort { + apiProtocol = kHttpProtocol + apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kRegionalGCIPAPIHost)" } else if useStaging { - protocolScheme = kHttpsProtocol - hostPrefix = kRegionalGCIPStagingAPIHost + apiProtocol = kHttpsProtocol + apiHostAndPathPrefix = kRegionalGCIPStagingAPIHost } else { - protocolScheme = kHttpsProtocol - hostPrefix = kRegionalGCIPAPIHost + apiProtocol = kHttpsProtocol + apiHostAndPathPrefix = kRegionalGCIPAPIHost } - - // Regionalized v2 path urlString = - "\(protocolScheme)//\(hostPrefix)/v2/projects/\(projectID ?? "projectID")" + "\(apiProtocol)//\(apiHostAndPathPrefix)/v2/projects/\(projectID ?? "projectID")" + "/locations/\(region)/tenants/\(tenant)/idpConfigs/\(endpoint)?key=\(apiKey)" + } + // legacy gcip existing logic + else if useIdentityPlatform { + if let emulatorHostAndPort = emulatorHostAndPort { + apiProtocol = kHttpProtocol + apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kIdentityPlatformAPIHost)" + } else if useStaging { + apiHostAndPathPrefix = kIdentityPlatformStagingAPIHost + apiProtocol = kHttpsProtocol + } else { + apiHostAndPathPrefix = kIdentityPlatformAPIHost + apiProtocol = kHttpsProtocol + } + urlString = "\(apiProtocol)//\(apiHostAndPathPrefix)/v2/\(endpoint)?key=\(apiKey)" } else { - // Legacy GCIP v1 branch - if let emu = emulatorHostAndPort { - protocolScheme = kHttpProtocol - hostPrefix = "\(emu)/\(kFirebaseAuthAPIHost)" + if let emulatorHostAndPort = emulatorHostAndPort { + apiProtocol = kHttpProtocol + apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kFirebaseAuthAPIHost)" } else if useStaging { - protocolScheme = kHttpsProtocol - hostPrefix = kFirebaseAuthStagingAPIHost + apiProtocol = kHttpsProtocol + apiHostAndPathPrefix = kFirebaseAuthStagingAPIHost } else { - protocolScheme = kHttpsProtocol - hostPrefix = kFirebaseAuthAPIHost + apiProtocol = kHttpsProtocol + apiHostAndPathPrefix = kFirebaseAuthAPIHost } urlString = - "\(protocolScheme)//\(hostPrefix)" + - "/identitytoolkit/v3/relyingparty/\(endpoint)?key=\(apiKey)" + "\(apiProtocol)//\(apiHostAndPathPrefix)/identitytoolkit/v3/relyingparty/\(endpoint)?key=\(apiKey)" } guard let returnURL = URL(string: "\(urlString)\(queryParams())") else { fatalError("Internal Auth error: Failed to generate URL for \(urlString)") @@ -124,14 +133,14 @@ class IdentityToolkitRequest { return returnURL } - // MARK: - Testing API - - /// For testing: override the global host for legacy flows - static var host: String { - get { gAPIHost } - set { gAPIHost = newValue } + /// Returns the request's configuration. + func requestConfiguration() -> AuthRequestConfiguration { + _requestConfiguration } + // MARK: Internal API for development + + static var host: String { gAPIHost } static func setHost(_ host: String) { gAPIHost = host } diff --git a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift index 441198a4d1f..3df5c7bf053 100644 --- a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift +++ b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift @@ -25,6 +25,9 @@ class IdentityToolkitRequestTests: XCTestCase { let kEndpoint = "endpoint" let kAPIKey = "APIKey" let kEmulatorHostAndPort = "emulatorhost:12345" + let kRegion = "us-central1" + let kTenantID = "tenant-id" + let kProjectID = "my-project-id" /** @fn testInitWithEndpointExpectedRequestURL @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the @@ -118,6 +121,8 @@ class IdentityToolkitRequestTests: XCTestCase { gcmSenderID: "00000000000000000-00000000000-000000000") options.apiKey = kAPIKey let nonDefaultApp = FirebaseApp(instanceWithName: "nonDefaultApp", options: options) + // Force initialize Auth for the non-default app to set the weak reference in + // AuthRequestConfiguration let nonDefaultAuth = Auth(app: nonDefaultApp) nonDefaultAuth.tenantID = "tenant-id" let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", @@ -128,4 +133,182 @@ class IdentityToolkitRequestTests: XCTestCase { useIdentityPlatform: true) XCTAssertEqual("tenant-id", request.tenantID) } + + // MARK: - R-GCIP specific tests + + /** @fn testInitWithRGCIPIExpectedRequestURL + @brief Tests the @c requestURL method for R-GCIP with region and tenant ID. + */ + func testInitWithRGCIPIExpectedRequestURL() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPApp", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth + .tenantID = kTenantID // Tenant ID is also needed in Auth for the request logic to pick it up + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIUseStagingExpectedRequestURL + @brief Tests the @c requestURL method for R-GCIP with region, tenant ID, and staging. + */ + func testInitWithRGCIPIUseStagingExpectedRequestURL() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppStaging", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration, + useStaging: true) + let expectedURL = + "https://staging-identityplatform.sandbox.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIUseEmulatorExpectedRequestURL + @brief Tests the @c requestURL method for R-GCIP with region, tenant ID, and emulator. + */ + func testInitWithRGCIPIUseEmulatorExpectedRequestURL() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmulator", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + requestConfiguration.emulatorHostAndPort = kEmulatorHostAndPort + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + let expectedURL = + "http://\(kEmulatorHostAndPort)/identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithoutProjectID + @brief Tests the @c requestURL method for R-GCIP when the project ID is not available in options. + */ + func testInitWithRGCIPIWithoutProjectID() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + // Project ID is not set in options + + let app = FirebaseApp(instanceWithName: "rGCIPAppWithoutProjectID", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + // The expected URL should use "projectID" as a placeholder + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/projectID" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithEmptyRegion + @brief Tests that the request falls back to the non-R-GCIP logic if the region is empty. + */ + func testInitWithRGCIPIWithEmptyRegion() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyRegion", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: "") // Empty region + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to empty region + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithEmptyTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is empty. + */ + func testInitWithRGCIPIWithEmptyTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + + let tenantConfig = TenantConfig(tenantId: "", + location: kRegion) // Empty tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to empty tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithNilTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is nil. + */ + func testInitWithRGCIPIWithNilTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + + let tenantConfig = TenantConfig(tenantId: "", + location: kRegion) // Nil tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to nil tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } } From 05d25cc0249fa4c4ac8fa16a6a33b13a8d0ecda2 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 30 May 2025 16:34:36 +0530 Subject: [PATCH 11/12] ading more IdentityToolkitRequestTests --- .../Unit/IdentityToolkitRequestTests.swift | 301 ++++++++++++------ 1 file changed, 205 insertions(+), 96 deletions(-) diff --git a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift index 3df5c7bf053..39b09ac646d 100644 --- a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift +++ b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift @@ -121,8 +121,7 @@ class IdentityToolkitRequestTests: XCTestCase { gcmSenderID: "00000000000000000-00000000000-000000000") options.apiKey = kAPIKey let nonDefaultApp = FirebaseApp(instanceWithName: "nonDefaultApp", options: options) - // Force initialize Auth for the non-default app to set the weak reference in - // AuthRequestConfiguration + // Force initialize Auth for the non-default app to set the weak reference in AuthRequestConfiguration let nonDefaultAuth = Auth(app: nonDefaultApp) nonDefaultAuth.tenantID = "tenant-id" let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", @@ -147,8 +146,7 @@ class IdentityToolkitRequestTests: XCTestCase { let app = FirebaseApp(instanceWithName: "rGCIPApp", options: options) // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration let auth = Auth(app: app) - auth - .tenantID = kTenantID // Tenant ID is also needed in Auth for the request logic to pick it up + auth.tenantID = kTenantID // Tenant ID is also needed in Auth for the request logic to pick it up let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", @@ -179,8 +177,7 @@ class IdentityToolkitRequestTests: XCTestCase { let request = IdentityToolkitRequest(endpoint: kEndpoint, requestConfiguration: requestConfiguration, useStaging: true) - let expectedURL = - "https://staging-identityplatform.sandbox.googleapis.com/v2/projects/\(kProjectID)" + + let expectedURL = "https://staging-identityplatform.sandbox.googleapis.com/v2/projects/\(kProjectID)" + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } @@ -204,8 +201,7 @@ class IdentityToolkitRequestTests: XCTestCase { requestConfiguration.emulatorHostAndPort = kEmulatorHostAndPort let request = IdentityToolkitRequest(endpoint: kEndpoint, requestConfiguration: requestConfiguration) - let expectedURL = - "http://\(kEmulatorHostAndPort)/identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + let expectedURL = "http://\(kEmulatorHostAndPort)/identityplatform.googleapis.com/v2/projects/\(kProjectID)" + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } @@ -214,101 +210,214 @@ class IdentityToolkitRequestTests: XCTestCase { @brief Tests the @c requestURL method for R-GCIP when the project ID is not available in options. */ func testInitWithRGCIPIWithoutProjectID() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - // Project ID is not set in options + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + // Project ID is not set in options - let app = FirebaseApp(instanceWithName: "rGCIPAppWithoutProjectID", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID + let app = FirebaseApp(instanceWithName: "rGCIPAppWithoutProjectID", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID - let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) - // The expected URL should use "projectID" as a placeholder - let expectedURL = "https://identityplatform.googleapis.com/v2/projects/projectID" + - "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + // The expected URL should use "projectID" as a placeholder + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/projectID" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } - /** @fn testInitWithRGCIPIWithEmptyRegion - @brief Tests that the request falls back to the non-R-GCIP logic if the region is empty. - */ - func testInitWithRGCIPIWithEmptyRegion() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyRegion", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID + /** @fn testInitWithRGCIPIWithEmptyRegion + @brief Tests that the request falls back to the non-R-GCIP logic if the region is empty. + */ + func testInitWithRGCIPIWithEmptyRegion() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyRegion", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID - let tenantConfig = TenantConfig(tenantId: kTenantID, location: "") // Empty region - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic - // will fail due to empty region - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = - "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } + let tenantConfig = TenantConfig(tenantId: kTenantID, location: "") // Empty region + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to empty region + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } - /** @fn testInitWithRGCIPIWithEmptyTenantIDInTenantConfig - @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is empty. - */ - func testInitWithRGCIPIWithEmptyTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyTenantIDTC", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + /** @fn testInitWithRGCIPIWithEmptyTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is empty. + */ + func testInitWithRGCIPIWithEmptyTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig - let tenantConfig = TenantConfig(tenantId: "", - location: kRegion) // Empty tenantId in tenant config - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic - // will fail due to empty tenantId - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = - "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } + let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Empty tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to empty tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } - /** @fn testInitWithRGCIPIWithNilTenantIDInTenantConfig - @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is nil. - */ - func testInitWithRGCIPIWithNilTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantIDTC", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + /** @fn testInitWithRGCIPIWithNilTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is nil. + */ + func testInitWithRGCIPIWithNilTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig - let tenantConfig = TenantConfig(tenantId: "", - location: kRegion) // Nil tenantId in tenant config - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic - // will fail due to nil tenantId - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = - "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } + let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Nil tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to nil tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithNilTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant config is nil. + */ + func testInitWithRGCIPIWithNilTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantConfig", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic requires tenantConfig + + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: nil) // Nil tenant config + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to nil tenant config + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIUseIdentityPlatform + @brief Tests that using `useIdentityPlatform` does not affect R-GCIP endpoint selection when R-GCIP configuration is present. + */ + func testInitWithRGCIPIUseIdentityPlatform() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppUseIP", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration, + useIdentityPlatform: true) // useIdentityPlatform is true + + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIAndLegacyTenantID + @brief Tests that R-GCIP logic prioritizes tenantId from `tenantConfig` over the one in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantID() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenant", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = "legacy-tenant-id" // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) // Tenant ID in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig + @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is nil in `tenantConfig` but present in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallback", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Nil tenantId in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + // Even though region is present, since tenantId in tenantConfig is nil, + // it should not use the R-GCIP path and fall back to default Auth endpoint. + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig + @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is empty in `tenantConfig` but present in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallbackEmpty", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Empty tenantId in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + // Even though region is present, since tenantId in tenantConfig is empty, + // it should not use the R-GCIP path and fall back to default Auth endpoint. + let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } } From f068bbb160a5b32bc5c5f659e9456bf0350ed2ea Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 30 May 2025 16:36:43 +0530 Subject: [PATCH 12/12] lint fix --- .../Unit/IdentityToolkitRequestTests.swift | 443 +++++++++--------- 1 file changed, 231 insertions(+), 212 deletions(-) diff --git a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift index 39b09ac646d..6e55ac3437b 100644 --- a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift +++ b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift @@ -121,7 +121,8 @@ class IdentityToolkitRequestTests: XCTestCase { gcmSenderID: "00000000000000000-00000000000-000000000") options.apiKey = kAPIKey let nonDefaultApp = FirebaseApp(instanceWithName: "nonDefaultApp", options: options) - // Force initialize Auth for the non-default app to set the weak reference in AuthRequestConfiguration + // Force initialize Auth for the non-default app to set the weak reference in + // AuthRequestConfiguration let nonDefaultAuth = Auth(app: nonDefaultApp) nonDefaultAuth.tenantID = "tenant-id" let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", @@ -146,7 +147,8 @@ class IdentityToolkitRequestTests: XCTestCase { let app = FirebaseApp(instanceWithName: "rGCIPApp", options: options) // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is also needed in Auth for the request logic to pick it up + auth + .tenantID = kTenantID // Tenant ID is also needed in Auth for the request logic to pick it up let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", @@ -177,7 +179,8 @@ class IdentityToolkitRequestTests: XCTestCase { let request = IdentityToolkitRequest(endpoint: kEndpoint, requestConfiguration: requestConfiguration, useStaging: true) - let expectedURL = "https://staging-identityplatform.sandbox.googleapis.com/v2/projects/\(kProjectID)" + + let expectedURL = + "https://staging-identityplatform.sandbox.googleapis.com/v2/projects/\(kProjectID)" + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } @@ -201,7 +204,8 @@ class IdentityToolkitRequestTests: XCTestCase { requestConfiguration.emulatorHostAndPort = kEmulatorHostAndPort let request = IdentityToolkitRequest(endpoint: kEndpoint, requestConfiguration: requestConfiguration) - let expectedURL = "http://\(kEmulatorHostAndPort)/identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + let expectedURL = + "http://\(kEmulatorHostAndPort)/identityplatform.googleapis.com/v2/projects/\(kProjectID)" + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } @@ -210,214 +214,229 @@ class IdentityToolkitRequestTests: XCTestCase { @brief Tests the @c requestURL method for R-GCIP when the project ID is not available in options. */ func testInitWithRGCIPIWithoutProjectID() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - // Project ID is not set in options - - let app = FirebaseApp(instanceWithName: "rGCIPAppWithoutProjectID", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID - - let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) - // The expected URL should use "projectID" as a placeholder - let expectedURL = "https://identityplatform.googleapis.com/v2/projects/projectID" + - "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + // Project ID is not set in options + + let app = FirebaseApp(instanceWithName: "rGCIPAppWithoutProjectID", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + // The expected URL should use "projectID" as a placeholder + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/projectID" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithEmptyRegion + @brief Tests that the request falls back to the non-R-GCIP logic if the region is empty. + */ + func testInitWithRGCIPIWithEmptyRegion() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyRegion", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: "") // Empty region + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to empty region + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithEmptyTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is empty. + */ + func testInitWithRGCIPIWithEmptyTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + + let tenantConfig = TenantConfig(tenantId: "", + location: kRegion) // Empty tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to empty tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithNilTenantIDInTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is nil. + */ + func testInitWithRGCIPIWithNilTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantIDTC", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig + + let tenantConfig = TenantConfig(tenantId: "", + location: kRegion) // Nil tenantId in tenant config + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to nil tenantId + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIWithNilTenantConfig + @brief Tests that the request falls back to the non-R-GCIP logic if the tenant config is nil. + */ + func testInitWithRGCIPIWithNilTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantConfig", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic requires tenantConfig + + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, + tenantConfig: nil) // Nil tenant config + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) // R-GCIP logic + // will fail due to nil tenant config + // Expecting fallback to the default Firebase Auth endpoint logic + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIUseIdentityPlatform + @brief Tests that using `useIdentityPlatform` does not affect R-GCIP endpoint selection when R-GCIP configuration is present. + */ + func testInitWithRGCIPIUseIdentityPlatform() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppUseIP", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID + + let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration, + useIdentityPlatform: true) // useIdentityPlatform is true + + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIAndLegacyTenantID + @brief Tests that R-GCIP logic prioritizes tenantId from `tenantConfig` over the one in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantID() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenant", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = "legacy-tenant-id" // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: kTenantID, + location: kRegion) // Tenant ID in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + + "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) } - /** @fn testInitWithRGCIPIWithEmptyRegion - @brief Tests that the request falls back to the non-R-GCIP logic if the region is empty. - */ - func testInitWithRGCIPIWithEmptyRegion() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyRegion", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID - - let tenantConfig = TenantConfig(tenantId: kTenantID, location: "") // Empty region - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to empty region - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIWithEmptyTenantIDInTenantConfig - @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is empty. - */ - func testInitWithRGCIPIWithEmptyTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppEmptyTenantIDTC", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig - - let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Empty tenantId in tenant config - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to empty tenantId - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIWithNilTenantIDInTenantConfig - @brief Tests that the request falls back to the non-R-GCIP logic if the tenant ID in tenant config is nil. - */ - func testInitWithRGCIPIWithNilTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantIDTC", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic uses tenantConfig - - let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Nil tenantId in tenant config - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to nil tenantId - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIWithNilTenantConfig - @brief Tests that the request falls back to the non-R-GCIP logic if the tenant config is nil. - */ - func testInitWithRGCIPIWithNilTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppNilTenantConfig", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID is set in Auth but R-GCIP logic requires tenantConfig - - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: nil) // Nil tenant config - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) // R-GCIP logic will fail due to nil tenant config - // Expecting fallback to the default Firebase Auth endpoint logic - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIUseIdentityPlatform - @brief Tests that using `useIdentityPlatform` does not affect R-GCIP endpoint selection when R-GCIP configuration is present. - */ - func testInitWithRGCIPIUseIdentityPlatform() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppUseIP", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID - - let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration, - useIdentityPlatform: true) // useIdentityPlatform is true - - let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + - "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIAndLegacyTenantID - @brief Tests that R-GCIP logic prioritizes tenantId from `tenantConfig` over the one in `Auth`. - */ - func testInitWithRGCIPIAndLegacyTenantID() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenant", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = "legacy-tenant-id" // Tenant ID in Auth - - let tenantConfig = TenantConfig(tenantId: kTenantID, location: kRegion) // Tenant ID in tenantConfig - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) - - let expectedURL = "https://identityplatform.googleapis.com/v2/projects/\(kProjectID)" + - "/locations/\(kRegion)/tenants/\(kTenantID)/idpConfigs/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig - @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is nil in `tenantConfig` but present in `Auth`. - */ - func testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallback", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID in Auth - - let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Nil tenantId in tenantConfig - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) - - // Even though region is present, since tenantId in tenantConfig is nil, - // it should not use the R-GCIP path and fall back to default Auth endpoint. - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } - - /** @fn testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig - @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is empty in `tenantConfig` but present in `Auth`. - */ - func testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig() { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.apiKey = kAPIKey - options.projectID = kProjectID - let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallbackEmpty", options: options) - // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration - let auth = Auth(app: app) - auth.tenantID = kTenantID // Tenant ID in Auth - - let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Empty tenantId in tenantConfig - let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", - auth: auth, tenantConfig: tenantConfig) - let request = IdentityToolkitRequest(endpoint: kEndpoint, - requestConfiguration: requestConfiguration) - - // Even though region is present, since tenantId in tenantConfig is empty, - // it should not use the R-GCIP path and fall back to default Auth endpoint. - let expectedURL = "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" - XCTAssertEqual(expectedURL, request.requestURL().absoluteString) - } + /** @fn testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig + @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is nil in `tenantConfig` but present in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantIDAndNoTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallback", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: "", location: kRegion) // Nil tenantId in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + // Even though region is present, since tenantId in tenantConfig is nil, + // it should not use the R-GCIP path and fall back to default Auth endpoint. + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } + + /** @fn testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig + @brief Tests that R-GCIP logic falls back to legacy tenant ID if tenantId is empty in `tenantConfig` but present in `Auth`. + */ + func testInitWithRGCIPIAndLegacyTenantIDAndEmptyTenantIDInTenantConfig() { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.apiKey = kAPIKey + options.projectID = kProjectID + let app = FirebaseApp(instanceWithName: "rGCIPAppLegacyTenantFallbackEmpty", options: options) + // Force initialize Auth for the app to set the weak reference in AuthRequestConfiguration + let auth = Auth(app: app) + auth.tenantID = kTenantID // Tenant ID in Auth + + let tenantConfig = TenantConfig(tenantId: "", + location: kRegion) // Empty tenantId in tenantConfig + let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID", + auth: auth, tenantConfig: tenantConfig) + let request = IdentityToolkitRequest(endpoint: kEndpoint, + requestConfiguration: requestConfiguration) + + // Even though region is present, since tenantId in tenantConfig is empty, + // it should not use the R-GCIP path and fall back to default Auth endpoint. + let expectedURL = + "https://www.googleapis.com/identitytoolkit/v3/relyingparty/\(kEndpoint)?key=\(kAPIKey)" + XCTAssertEqual(expectedURL, request.requestURL().absoluteString) + } }