Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2457,8 +2457,8 @@ public extension Auth {
///
/// - Parameters:
/// - tenantId: The ID of the tenant.
/// - location: The location of the tenant. Defaults to "prod-global".
public init(tenantId: String, location: String = "prod-global") {
/// - location: The location of the tenant. Defaults to "global".
public init(tenantId: String, location: String = "global") {
self.location = location
self.tenantId = tenantId
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 regionalGCIPAPIHost = "identityplatform.googleapis.com"
private let regionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"

// MARK: - ExchangeTokenRequest

/// A request to exchange a third-party OIDC ID token for a Firebase ID token.
///
/// This structure encapsulates the parameters required to call the
/// `exchangeOidcToken` endpoint on the regionalized Identity Platform backend.
/// It conforms to `AuthRPCRequest`, providing the necessary properties and
/// methods for the authentication backend to perform the request.
/// This is used for the BYO-CIAM (regionalized GCIP) flow.
@available(iOS 13, *)
struct ExchangeTokenRequest: AuthRPCRequest {
/// The type of the expected response.
typealias Response = ExchangeTokenResponse

/// The customer application redirects the user to the OIDC provider,
/// and receives this idToken for the user upon successful authentication.
let idToken: String

/// The ID of the Identity Provider configuration, as configured for the tenant.
let idpConfigID: String

/// The auth configuration for the request, holding API key, etc.
let config: AuthRequestConfiguration

/// Flag for whether to use the staging backend.
let useStaging: Bool

/// The base URL components for the request, will be used to construct the final URL.
private var baseURLComponents: URLComponents {
var components = URLComponents()
components.scheme = "https"
components.host = useStaging ? regionalGCIPStagingAPIHost : regionalGCIPAPIHost
return components
}

/// Initializes an `ExchangeTokenRequest`.
///
/// - Parameters:
/// - idToken: The third-party OIDC ID token from the external IdP to be exchanged.
/// - idpConfigID: The ID of the IdP configuration.
/// - config: The `AuthRequestConfiguration`.
/// - useStaging: Set to `true` to target the staging environment. Defaults to `false`.
init(idToken: String,
idpConfigID: String,
config: AuthRequestConfiguration,
useStaging: Bool = false) {
self.idToken = idToken
self.idpConfigID = idpConfigID
self.config = config
self.useStaging = useStaging
}

/// The unencoded HTTP request body for the API.
var unencodedHTTPRequestBody: [String: AnyHashable]? {
return ["id_token": idToken]
}

/// Constructs the full URL for the `ExchangeOidcToken` API endpoint.
///
/// - Important: This method will cause a `fatalError` if the `location`, `tenantId`, or
/// `projectID` are missing from the configuration, as they are essential for
/// constructing a valid regional endpoint URL.
/// - Returns: The fully constructed `URL` for the API request.
func requestURL() -> URL {
guard let location = config.tenantConfig?.location,
let tenant = config.tenantConfig?.tenantId,
let project = config.auth?.app?.options.projectID
else {
fatalError(
"Internal Error: ExchangeTokenRequest requires `location`, `tenantId`, and `projectID`."
)
}
var components = baseURLComponents
if location != "global" {
components.host = "\(location)-\(components.host ?? "")"
}
let locationPath = location == "global" ? "global" : location
components
.path =
"/v2beta/projects/\(project)/locations/\(locationPath)/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken"
components.queryItems = [URLQueryItem(name: "key", value: config.apiKey)]
guard let url = components.url else {
fatalError("Failed to create URL for ExchangeTokenRequest")
}
return url
}

/// Returns the request configuration.
func requestConfiguration() -> AuthRequestConfiguration {
return config
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// 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

// MARK: - ExchangeTokenResponse

/// An internal response containing the result of a successful OIDC token exchange.
///
/// Contains the Firebase ID token and its expiration time.
/// This struct implements `AuthRPCResponse` to parse the JSON payload from the
/// `exchangeOidcToken` endpoint.
@available(iOS 13, *)
struct ExchangeTokenResponse: AuthRPCResponse {
/// The exchanged firebase access token.
let firebaseToken: String

/// The lifetime of the token in seconds.
let expiresIn: TimeInterval

/// The calculated date and time when the token expires.
let expirationDate: Date

/// Initializes an `ExchangeTokenResponse` by parsing a dictionary from a JSON
/// payload.
///
/// - Parameter dictionary: The dictionary representing the JSON response from server.
/// - Throws: `AuthErrorUtils.unexpectedResponse` if the required fields
/// (like "idToken", "expiresIn") are missing, have unexpected types
init(dictionary: [String: AnyHashable]) throws {
guard let token = dictionary["idToken"] as? String else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
firebaseToken = token
guard let expiresInString = dictionary["expiresIn"] as? String,
let expiresInInterval = TimeInterval(expiresInString) else {
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
}
expiresIn = expiresInInterval
expirationDate = Date().addingTimeInterval(expiresIn)
}
}
Loading