Skip to content

Enable OIDC Token Exchange for BYO-CIAM (R-GCIP) #14983

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: exchange-token-implementation
Choose a base branch
from
Open
Changes from all 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
126 changes: 117 additions & 9 deletions FirebaseAuth/Sources/Swift/Auth/Auth.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,22 @@ public struct TenantConfig: Sendable {
/// - tenantId: The ID of the tenant.
/// - location: The location of the tenant. Defaults to "prod-global".
public init(tenantId: String, location: String = "prod-global") {
self.location = location
self.tenantId = tenantId
self.location = location
}
}

/// Represents the result of a successful OIDC token exchange, containing a Firebase ID token
/// and its expiration.
public struct FirebaseToken: Sendable {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point in the TenantConfig PR, is the nesting (e.g. Auth.FirebaseToken) intentional?

/// The Firebase ID token string.
public let token: String
/// The date at which the Firebase ID token expires.
public let expirationDate: Date

init(token: String, expirationDate: Date) {
self.token = token
self.expirationDate = expirationDate
}
}

Expand Down Expand Up @@ -186,16 +200,10 @@ public struct TenantConfig: Sendable {
/// 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` configured for a specific Regional Google Cloud
/// Identity Platform (R-GCIP) tenant.
///
/// Use this method to create an `Auth` instance that interacts with a regionalized
/// authentication backend instead of the default endpoint.
///
/// Gets the auth object for a `FirebaseApp` with an optional `TenantConfig`.
/// - Parameters:
/// - app: The Firebase app instance.
/// - tenantConfig: The configuration for the R-GCIP tenant, specifying the tenant ID and its
/// location.
/// - 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)
Expand Down Expand Up @@ -2461,3 +2469,103 @@ public struct TenantConfig: Sendable {
/// 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 ID token.
///
/// This method is used for Bring Your Own CIAM (BYO-CIAM) in Regionalized GCIP (R-GCIP),
/// where the `Auth` instance must be configured with a `TenantConfig`, including `location`
/// and `tenantId`, typically by using `Auth.auth(app:tenantConfig:)`.
///
/// Unlike standard sign-in methods, this flow *does not* create or update a `User` object and
/// *does not* set `CurrentUser` on the `Auth` instance. It only returns a Firebase token.
///
/// - Parameters:
/// - oidcToken: The OIDC ID token obtained from the third-party identity provider.
/// - idpConfigId: The ID of the Identity Provider configuration within your GCIP tenant
/// (e.g., "oidc.my-provider").
/// - useStaging: A Boolean value indicating whether to use the staging Identity Platform
/// backend. Defaults to `false`.
/// - completion: A closure that is called asynchronously on the main thread with either a
/// `FirebaseToken` on success or an `Error` on failure.
func exchangeToken(idToken: String,
idpConfigId: String,
useStaging: Bool = false,
completion: @escaping (FirebaseToken?, Error?) -> Void) {
Comment on lines +2492 to +2495
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer if we don't introduce new API that uses completion handlers. As least initially, new async API should be async/await only unless there a reason for completion handler (e.g. objc support, etc.)

/// 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: "Auth instance must be configured with a TenantConfig, including location and tenantId, to use exchangeToken."
))
)
return
}
let request = ExchangeTokenRequest(
idToken: idToken,
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 ID token for a Firebase ID token using Swift concurrency.
///
/// This method is used for Bring Your Own CIAM (BYO-CIAM) in Regionalized GCIP (R-GCIP),
/// where the `Auth` instance must be configured with a `TenantConfig`, including `location`
/// and `tenantId`, typically by using `Auth.auth(app:tenantConfig:)`.
///
/// Unlike standard sign-in methods, this flow *does not* create or update a `User`object and
/// *does not* set `CurrentUser` on the `Auth` instance. It only returns a Firebase token.
///
/// - Parameters:
/// - oidcToken: The OIDC ID token obtained from the third-party identity provider.
/// - idpConfigId: The ID of the Identity Provider configuration within your GCIP tenant
/// (e.g., "oidc.my-provider").
/// - useStaging: A Boolean value indicating whether to use the staging Identity Platform
/// backend. Defaults to `false`.
/// - Returns: A `FirebaseToken` containing the Firebase ID token and its expiration date.
/// - Throws: An error if the `Auth` instance is not configured for R-GCIP, if the network
/// call fails, or if the token response parsing fails.
func exchangeToken(idToken: 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.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think this should instead cause a fatal error? When do we anticipate this case triggering, during development or possibly in live app's execution?

}
let request = ExchangeTokenRequest(
idToken: idToken,
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
}
}
}
Loading