diff --git a/ExchangeTokensRequestTests.swift b/ExchangeTokensRequestTests.swift new file mode 100644 index 00000000000..2027475823b --- /dev/null +++ b/ExchangeTokensRequestTests.swift @@ -0,0 +1,151 @@ +// Copyright 2023 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 +import XCTest + +@testable import FirebaseAuth +import FirebaseCore + +/// @class ExchangeTokenRequestTests +/// @brief Tests for the @c ExchangeTokenRequest struct. +@available(iOS 13, *) +class ExchangeTokenRequestTests: XCTestCase { + // MARK: - Constants for Testing + + let kAPIKey = "test-api-key" + let kProjectID = "test-project-id" + let kLocation = "asia-northeast1" + let kTenantID = "test-tenant-id-123" + let kCustomToken = "a-very-long-and-secure-oidc-token-string" + let kIdpConfigId = "oidc.my-test-provider" + + let kProductionHost = "identityplatform.googleapis.com" + let kStagingHost = "staging-identityplatform.sandbox.googleapis.com" + + // MARK: - Test Cases + + func testProductionURLIsCorrectlyConstructed() { + let (auth, app) = createTestAuthInstance( + projectID: kProjectID, + location: kLocation, + tenantId: kTenantID + ) + _ = app + + let request = ExchangeTokenRequest( + customToken: kCustomToken, + idpConfigID: kIdpConfigId, + config: auth.requestConfiguration, + useStaging: false + ) + + let expectedHost = "\(kLocation)-\(kProductionHost)" + let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" + + "/locations/\(kLocation)/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)" + + XCTAssertEqual(request.requestURL().absoluteString, expectedURL) + } + + func testProductionURLIsCorrectlyConstructedForGlobalLocation() { + let (auth, app) = createTestAuthInstance( + projectID: kProjectID, + location: "prod-global", + tenantId: kTenantID + ) + _ = app + + let request = ExchangeTokenRequest( + customToken: kCustomToken, + idpConfigID: kIdpConfigId, + config: auth.requestConfiguration, + useStaging: false + ) + + let expectedHost = kProductionHost + let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" + + "/locations/prod-global/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)" + + XCTAssertEqual(request.requestURL().absoluteString, expectedURL) + } + + func testStagingURLIsCorrectlyConstructed() { + let (auth, app) = createTestAuthInstance( + projectID: kProjectID, + location: kLocation, + tenantId: kTenantID + ) + _ = app + + let request = ExchangeTokenRequest( + customToken: kCustomToken, + idpConfigID: kIdpConfigId, + config: auth.requestConfiguration, + useStaging: true + ) + + let expectedHost = "\(kLocation)-\(kStagingHost)" + let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" + + "/locations/\(kLocation)/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)" + + XCTAssertEqual(request.requestURL().absoluteString, expectedURL) + } + + func testUnencodedHTTPBodyIsCorrect() { + let (auth, app) = createTestAuthInstance( + projectID: kProjectID, + location: kLocation, + tenantId: kTenantID + ) + _ = app + + let request = ExchangeTokenRequest( + customToken: kCustomToken, + idpConfigID: kIdpConfigId, + config: auth.requestConfiguration + ) + + let body = request.unencodedHTTPRequestBody + XCTAssertNotNil(body) + XCTAssertEqual(body?.count, 1) + XCTAssertEqual(body?["custom_token"] as? String, kCustomToken) + } + + // MARK: - Helper Function + + private func createTestAuthInstance(projectID: String?, location: String?, + tenantId: String?) -> (auth: Auth, app: FirebaseApp) { + let appName = "TestApp-\(UUID().uuidString)" + let options = FirebaseOptions( + googleAppID: "1:1234567890:ios:abcdef123456", + gcmSenderID: "1234567890" + ) + options.apiKey = kAPIKey + if let projectID = projectID { + options.projectID = projectID + } + + if FirebaseApp.app(name: appName) != nil { + FirebaseApp.app(name: appName)?.delete { _ in } + } + let app = FirebaseApp(instanceWithName: appName, options: options) + + let auth = Auth(app: app) + auth.app = app + auth.requestConfiguration.location = location + auth.requestConfiguration.tenantId = tenantId + + return (auth, app) + } +} diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index 5d8050cc891..5f8d33ce5f1 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 FirebaseToken: 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,6 +195,20 @@ extension Auth: AuthInterop { /// Gets the `FirebaseApp` object that this auth object is connected to. @objc public internal(set) weak var app: FirebaseApp? + /// Gets the auth object for a `FirebaseApp` with an optional `TenantConfig`. + /// - Parameters: + /// - 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 { + let auth = auth(app: app) + kAuthGlobalWorkQueue.sync { + auth.requestConfiguration.location = tenantConfig.location + auth.requestConfiguration.tenantId = tenantConfig.tenantId + } + return auth + } + /// Synchronously gets the cached current user, or null if there is none. @objc public var currentUser: User? { kAuthGlobalWorkQueue.sync { @@ -2425,3 +2464,89 @@ 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. + /// + /// 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: + /// - 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, + useStaging: Bool = false, + completion: @escaping (FirebaseToken?, Error?) -> Void) { + // Ensure R-GCIP is configured with location and tenant ID + guard let _ = requestConfiguration.location, + let _ = requestConfiguration.tenantId + else { + Auth.wrapMainAsync( + callback: completion, + with: .failure(AuthErrorUtils + .operationNotAllowedError(message: "R-GCIP is not configured.")) + ) + return + } + let request = ExchangeTokenRequest( + customToken: customToken, + idpConfigID: idpConfigId, + config: requestConfiguration, + useStaging: true + ) + Task { + do { + let response = try await backend.call(with: request) + let firebaseToken = FirebaseToken( + token: response.firebaseToken, + expirationDate: response.expirationDate + ) + Auth.wrapMainAsync(callback: completion, with: .success(firebaseToken)) + } catch { + Auth.wrapMainAsync(callback: completion, with: .failure(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`. + /// Unlike other sign-in methods, this flow *does not* create or update a `User` object. + /// + /// - Parameters: + /// - 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, + useStaging: Bool = false) async throws -> FirebaseToken { + // Ensure R-GCIP is configured with location and tenant ID + guard let _ = requestConfiguration.location, + let _ = requestConfiguration.tenantId + else { + throw AuthErrorUtils.operationNotAllowedError(message: "R-GCIP is not configured.") + } + let request = ExchangeTokenRequest( + customToken: customToken, + idpConfigID: idpConfigId, + config: requestConfiguration, + useStaging: true + ) + do { + let response = try await backend.call(with: request) + return FirebaseToken( + 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 91f99c266f8..d1189b25af7 100644 --- a/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift +++ b/FirebaseAuth/Sources/Swift/Backend/AuthRequestConfiguration.swift @@ -44,15 +44,24 @@ final class AuthRequestConfiguration { /// If set, the local emulator host and port to point to instead of the remote backend. var emulatorHostAndPort: String? + /// R-GCIP location, 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: 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..ec543896b64 100644 --- a/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift +++ b/FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift @@ -30,7 +30,8 @@ private let kFirebaseAuthStagingAPIHost = "staging-www.sandbox.googleapis.com" private let kIdentityPlatformStagingAPIHost = "staging-identitytoolkit.sandbox.googleapis.com" -/// Represents a request to an identity toolkit endpoint. +/// 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 { /// Gets the RPC's endpoint. @@ -39,7 +40,7 @@ class IdentityToolkitRequest { /// 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. @@ -74,6 +75,7 @@ class IdentityToolkitRequest { let apiHostAndPathPrefix: String let urlString: String let emulatorHostAndPort = _requestConfiguration.emulatorHostAndPort + // legacy gcip logic if useIdentityPlatform { if let emulatorHostAndPort = emulatorHostAndPort { apiProtocol = kHttpProtocol diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift new file mode 100644 index 00000000000..34f28777e2d --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift @@ -0,0 +1,98 @@ +// 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 + +private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com" +private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com" + +/// 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 OIDC provider's Authorization code or Id Token to exchange. + let customToken: String + + /// The ExternalUserDirectoryId corresponding to the OIDC custom Token. + let idpConfigID: String + + /// The configuration for the request, holding API key, tenant, etc. + let config: AuthRequestConfiguration + + let useStaging: Bool + + init(customToken: String, + idpConfigID: String, + config: AuthRequestConfiguration, + useStaging: Bool = false) { + self.idpConfigID = idpConfigID + self.customToken = customToken + self.config = config + self.useStaging = useStaging + } + + /// The unencoded HTTP request body for the API. + var unencodedHTTPRequestBody: [String: AnyHashable]? { + return ["custom_token": customToken] + } + + /// 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 location = config.location, + let tenant = config.tenantId, + let projectId = config.auth?.app?.options.projectID + else { + fatalError( + "exchangeOidcToken requires `location`, `tenantID` & `projectID` to be configured." + ) + } + let host: String + if useStaging { + if location == "prod-global" { + host = kRegionalGCIPStagingAPIHost + } else { + host = "\(location)-\(kRegionalGCIPStagingAPIHost)" + } + } else { + if location == "prod-global" { + host = kRegionalGCIPAPIHost + } else { + host = "\(location)-\(kRegionalGCIPAPIHost)" + } + } + + let path = "/v2alpha/projects/\(projectId)/locations/\(location)" + + "/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..d730d66ab50 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift @@ -0,0 +1,44 @@ +// 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 + +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 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) + } + firebaseToken = token + expiresIn = (dictionary["expiresIn"] as? TimeInterval) ?? 3600 + expirationDate = Date().addingTimeInterval(expiresIn) + } +} diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift index 5683ed96331..a37f3c5d240 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift @@ -25,8 +25,10 @@ class AppManager { private var otherApp: FirebaseApp var app: FirebaseApp + let tenantConfig = TenantConfig(tenantId: "tenantId", location: "us-east1") + func auth() -> Auth { - return Auth.auth(app: app) + return Auth.auth(app: app, tenantConfig: tenantConfig) } private init() { diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift index 5e9f8af3cf0..84175b42f78 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift @@ -53,6 +53,7 @@ enum AuthMenu: String { case phoneEnroll case totpEnroll case multifactorUnenroll + case exchangeToken // More intuitively named getter for `rawValue`. var id: String { rawValue } @@ -139,6 +140,9 @@ enum AuthMenu: String { return "TOTP Enroll" case .multifactorUnenroll: return "Multifactor unenroll" + // R-GCIP Exchange Token + case .exchangeToken: + return "Exchange Token" } } @@ -220,6 +224,8 @@ enum AuthMenu: String { self = .totpEnroll case "Multifactor unenroll": self = .multifactorUnenroll + case "Exchange Token": + self = .exchangeToken default: return nil } @@ -354,9 +360,17 @@ class AuthMenuData: DataSourceProvidable { return Section(headerDescription: header, items: items) } + static var exchangeTokenSection: Section { + let header = "Exchange Token [Regionalized]" + let items: [Item] = [ + Item(title: AuthMenu.exchangeToken.name), + ] + return Section(headerDescription: header, items: items) + } + static let sections: [Section] = [settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection, - customAuthDomainSection, appSection, oobSection, multifactorSection] + customAuthDomainSection, appSection, oobSection, multifactorSection, exchangeTokenSection] static var authLinkSections: [Section] { let allItems = [providerSection, emailPasswordSection, otherSection].flatMap { $0.items } diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift index 240346b6975..71755c76eaa 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/ViewControllers/AuthViewController.swift @@ -191,6 +191,9 @@ class AuthViewController: UIViewController, DataSourceProviderDelegate { case .multifactorUnenroll: mfaUnenroll() + + case .exchangeToken: + callExchangeToken() } } @@ -1085,4 +1088,42 @@ extension AuthViewController: ASAuthorizationControllerDelegate, func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { return view.window! } + + func callExchangeToken() { + Task { + do { + // 1. Prompt for the first piece of input and wait for the result. + guard let customToken = await showTextInputPrompt(with: "Enter Custom Token:") else { + // If the user cancels or enters nothing, stop the process. + print("Token exchange cancelled: Custom Token was not provided.") + return + } + + // 2. Prompt for the second piece of input and wait for the result. + guard let idpConfigId = await showTextInputPrompt(with: "Enter IDP Config ID:") else { + // If the user cancels or enters nothing, stop the process. + print("Token exchange cancelled: IDP Config ID was not provided.") + return + } + + // 3. Both inputs were provided, so now we can make the API call. + let result = try await AppManager.shared.auth().exchangeToken( + customToken: customToken, + idpConfigId: idpConfigId, + useStaging: true + ) + print("Token exchange successful") + print("Firebase Token: \(result.token)") + + // Let the user know it succeeded. + showAlert(for: "Token Exchange Succeeded", + message: "Review the console logs for the new Firebase Token.") + + } catch { + print("Failed to exchange token: \(error)") + showAlert(for: "Token Exchange Error", + message: error.localizedDescription) + } + } + } } diff --git a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift index 441198a4d1f..2b27be36e0e 100644 --- a/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift +++ b/FirebaseAuth/Tests/Unit/IdentityToolkitRequestTests.swift @@ -25,10 +25,15 @@ class IdentityToolkitRequestTests: XCTestCase { let kEndpoint = "endpoint" let kAPIKey = "APIKey" let kEmulatorHostAndPort = "emulatorhost:12345" + let kLocation = "us-central1" + let kTenantID = "tenant-id" + let kProjectID = "my-project-id" + let kCustomToken = "custom-token" + let kIdpConfigId = "idpConfigId" /** @fn testInitWithEndpointExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs. */ func testInitWithEndpointExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -40,8 +45,8 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testInitWithEndpointUseStagingExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs when the staging endpoint is specified. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs when the staging endpoint is specified. */ func testInitWithEndpointUseStagingExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -54,8 +59,8 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testInitWithEndpointUseIdentityPlatformExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs when the Identity Platform endpoint is specified. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs when the Identity Platform endpoint is specified. */ func testInitWithEndpointUseIdentityPlatformExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -67,8 +72,8 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testInitWithEndpointUseIdentityPlatformUseStagingExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs when the Identity Platform and staging endpoint is specified. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs when the Identity Platform and staging endpoint is specified. */ func testInitWithEndpointUseIdentityPlatformUseStagingExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -82,8 +87,8 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testInitWithEndpointUseEmulatorExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs when the emulator is used. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs when the emulator is used. */ func testInitWithEndpointUseEmulatorExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -96,8 +101,8 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testInitWithEndpointUseIdentityPlatformUseEmulatorExpectedRequestURL - @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the - request inputs when the emulator is used with the Identity Platform endpoint. + @brief Tests the @c requestURL method to make sure the URL it produces corresponds to the + request inputs when the emulator is used with the Identity Platform endpoint. */ func testInitWithEndpointUseIdentityPlatformUseEmulatorExpectedRequestURL() { let requestConfiguration = AuthRequestConfiguration(apiKey: kAPIKey, appID: "appID") @@ -111,13 +116,15 @@ class IdentityToolkitRequestTests: XCTestCase { } /** @fn testExpectedTenantIDWithNonDefaultFIRApp - @brief Tests the request correctly populated the tenant ID from a non default app. + @brief Tests the request correctly populated the tenant ID from a non default app. */ func testExpectedTenantIDWithNonDefaultFIRApp() { let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", 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",