Skip to content

Commit 0fac248

Browse files
committed
addressing review comments
1 parent 860fb46 commit 0fac248

File tree

3 files changed

+117
-46
lines changed

3 files changed

+117
-46
lines changed

FirebaseAuth/Sources/Swift/Auth/Auth.swift

Lines changed: 88 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -189,25 +189,25 @@ extension Auth: AuthInterop {
189189
}
190190

191191
/// Holds configuration for a R-GCIP tenant.
192-
public struct TenantConfig {
193-
public let location: String /// The location of the tenant.
192+
public struct TenantConfig: Sendable {
194193
public let tenantId: String /// The ID of the tenant.
194+
public let location: String /// The location of the tenant.
195195

196196
/// Initializes a `TenantConfig` instance.
197197
/// - Parameters:
198198
/// - location: The location of the tenant, defaults to "prod-global".
199199
/// - tenantId: The ID of the tenant.
200-
public init(location: String = "prod-global", tenantId: String) {
200+
public init(tenantId: String, location: String = "prod-global") {
201201
self.location = location
202202
self.tenantId = tenantId
203203
}
204204
}
205205

206206
/// Holds a Firebase ID token and its expiration.
207-
public struct AuthExchangeToken {
207+
public struct AuthExchangeToken: Sendable {
208208
public let token: String
209209
public let expirationDate: Date?
210-
init(token: String, expirationDate: Date?) {
210+
init(token: String, expirationDate: Date) {
211211
self.token = token
212212
self.expirationDate = expirationDate
213213
}
@@ -2471,37 +2471,91 @@ extension Auth: AuthInterop {
24712471

24722472
@available(iOS 13, *)
24732473
public extension Auth {
2474-
/// Exchanges a third-party OIDC token for a Firebase STS token.
2474+
2475+
/// Exchanges a third-party OIDC token for a Firebase STS token using a completion handler.
2476+
///
2477+
/// This method is used in R-GCIP (multi-tenant) environments where the `Auth` instance must
2478+
/// be configured with a `TenantConfig`, including `location` and `tenantId`.
24752479
///
2476-
/// Requires the `Auth` instance to be configured with a `TenantConfig` for R-GCIP.
24772480
/// - Parameters:
2478-
/// - idpConfigID: The ID of the OIDC provider configuration.
2479-
/// - ciamOidcToken: The OIDC token to exchange.
2480-
/// - completion: Called with the Firebase ID token or an error.
2481-
@objc func exchangeToken(_ idpConfigID: String,
2482-
_ ciamOidcToken: String,
2483-
completion: @escaping (String?, Error?) -> Void) {
2484-
// Check if R-GCIP (location and tenantId) is configured.
2485-
guard let location = requestConfiguration.location,
2486-
let tenantId = requestConfiguration.tenantId
2487-
else {
2488-
completion(nil, AuthErrorUtils.operationNotAllowedError(
2489-
message: "Set location & tenantId first"
2490-
))
2491-
return
2492-
}
2493-
let request = ExchangeTokenRequest(
2494-
idpConfigID: idpConfigID,
2495-
idToken: ciamOidcToken,
2496-
config: requestConfiguration
2497-
)
2498-
Task {
2499-
do {
2500-
let resp = try await backend.call(with: request)
2501-
DispatchQueue.main.async { completion(resp.firebaseToken, nil) }
2502-
} catch {
2503-
DispatchQueue.main.async { completion(nil, error) }
2481+
/// - idToken: The OIDC token received from the third-party Identity Provider (IdP).
2482+
/// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase.
2483+
/// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`.
2484+
public func exchangeToken(
2485+
idToken: String,
2486+
idpConfigId: String,
2487+
completion: @escaping (AuthTokenResult?, Error?) -> Void
2488+
) {
2489+
// Ensure R-GCIP is configured with location and tenant ID
2490+
guard let location = requestConfiguration.location,
2491+
let tenantId = requestConfiguration.tenantId
2492+
else {
2493+
completion(nil, AuthErrorCode.operationNotAllowed)
2494+
return
25042495
}
2505-
}
2496+
2497+
// Build the exchange token request
2498+
let request = ExchangeTokenRequest(
2499+
idToken: idToken,
2500+
idpConfigID: idpConfigId,
2501+
config: requestConfiguration
2502+
)
2503+
2504+
// Perform the token exchange asynchronously
2505+
Task {
2506+
do {
2507+
let response = try await backend.call(with: request)
2508+
do {
2509+
// Try to parse the Firebase token response
2510+
let authTokenResult = try AuthTokenResult.tokenResult(token: response.firebaseToken)
2511+
DispatchQueue.main.async {
2512+
completion(authTokenResult, nil)
2513+
}
2514+
} catch {
2515+
// Failed to parse JWT
2516+
DispatchQueue.main.async {
2517+
completion(nil, AuthErrorCode.malformedJWT)
2518+
}
2519+
}
2520+
} catch {
2521+
// Backend call failed
2522+
DispatchQueue.main.async {
2523+
completion(nil, error)
2524+
}
2525+
}
2526+
}
2527+
}
2528+
2529+
/// Exchanges a third-party OIDC token for a Firebase STS token using Swift concurrency.
2530+
///
2531+
/// This async variant performs the same operation as the completion-based method but returns
2532+
/// the result directly and throws on failure.
2533+
///
2534+
/// The `Auth` instance must be configured with `TenantConfig` containing `location` and `tenantId`.
2535+
///
2536+
/// - Parameters:
2537+
/// - idToken: The OIDC token received from the third-party Identity Provider (IdP).
2538+
/// - idpConfigId: The identifier of the OIDC provider configuration defined in Firebase.
2539+
/// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details.
2540+
/// - Throws: An error if R-GCIP is not configured, if the network call fails,
2541+
/// or if the token parsing fails.
2542+
public func exchangeToken(idToken: String, idpConfigId: String) async throws -> AuthTokenResult {
2543+
// Ensure R-GCIP is configured with location and tenant ID
2544+
guard let location = requestConfiguration.location,
2545+
let tenantId = requestConfiguration.tenantId
2546+
else {
2547+
throw AuthErrorCode.operationNotAllowed
2548+
}
2549+
2550+
// Build the exchange token request
2551+
let request = ExchangeTokenRequest(
2552+
idToken: idToken,
2553+
idpConfigID: idpConfigId,
2554+
config: requestConfiguration
2555+
)
2556+
2557+
// Perform the backend call and return parsed token
2558+
let response = try await backend.call(with: request)
2559+
return try AuthTokenResult.tokenResult(token: response.firebaseToken)
25062560
}
25072561
}

FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,11 @@ struct ExchangeTokenRequest: AuthRPCRequest {
2525
/// The type of the expected response.
2626
typealias Response = ExchangeTokenResponse
2727

28-
/// The identifier of the OIDC provider configuration.
29-
private let idpConfigID: String
30-
3128
/// The third-party OIDC token to exchange.
3229
private let idToken: String
30+
31+
/// The identifier of the OIDC provider configuration.
32+
private let idpConfigID: String
3333

3434
/// The configuration for the request, holding API key, tenant, etc.
3535
private let config: AuthRequestConfiguration
@@ -40,8 +40,8 @@ struct ExchangeTokenRequest: AuthRPCRequest {
4040
/// - idpConfigID: The identifier of the OIDC provider configuration.
4141
/// - idToken: The third-party OIDC token to exchange.
4242
/// - config: The configuration for the request.
43-
init(idpConfigID: String,
44-
idToken: String,
43+
init(idToken: String,
44+
idpConfigID: String,
4545
config: AuthRequestConfiguration) {
4646
self.idpConfigID = idpConfigID
4747
self.idToken = idToken

FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenResponse.swift

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,31 @@
1414

1515
import Foundation
1616

17-
/// Response containing the new Firebase STS token.
17+
import Foundation
18+
19+
/// Response containing the new Firebase STS token and its expiration time in seconds.
1820
@available(iOS 13, *)
1921
struct ExchangeTokenResponse: AuthRPCResponse {
20-
let firebaseToken: String
21-
init(dictionary: [String: AnyHashable]) throws {
22-
guard let token = dictionary["idToken"] as? String else {
23-
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
22+
/// The Firebase ID token.
23+
let firebaseToken: String
24+
25+
/// The time interval (in *seconds*) until the token expires.
26+
let expiresIn: TimeInterval
27+
28+
/// The expiration date of the token, calculated from `expiresInSeconds`.
29+
let expirationDate: Date
30+
31+
/// Initializes a new ExchangeTokenResponse from a dictionary.
32+
///
33+
/// - Parameter dictionary: The dictionary representing the JSON response from the server.
34+
/// - Throws: `AuthErrorUtils.unexpectedResponse` if the dictionary is missing required fields
35+
/// or contains invalid data.
36+
init(dictionary: [String: AnyHashable]) throws {
37+
guard let token = dictionary["idToken"] as? String else {
38+
throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary)
39+
}
40+
self.firebaseToken = token
41+
expiresIn = (dictionary["expiresIn"] as? TimeInterval) ?? 3600
42+
expirationDate = Date().addingTimeInterval(expiresIn)
2443
}
25-
firebaseToken = token
26-
}
2744
}

0 commit comments

Comments
 (0)