Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
0eeb7a2
Add config support
daymxn Jul 9, 2025
6c878ff
fmt
daymxn Jul 9, 2025
e6de038
Migrate to nested config
daymxn Jul 11, 2025
086e673
Mix invalid references
daymxn Jul 11, 2025
4c9d28b
Fix invalid reference (again)
daymxn Jul 11, 2025
8a06ee5
fmt
daymxn Jul 11, 2025
8659455
Remove unnecessary Encodable
daymxn Jul 15, 2025
56d1f5f
Update AppCheckOptions.swift
daymxn Jul 16, 2025
1b6c09c
Update FirebaseAI.swift
daymxn Jul 16, 2025
1dfbce5
Update FirebaseAI/Sources/AppCheckOptions.swift
daymxn Aug 13, 2025
6dddd09
Update FirebaseAI/Sources/AppCheckOptions.swift
daymxn Aug 13, 2025
a188a79
Update FirebaseAI/Sources/AppCheckOptions.swift
daymxn Aug 13, 2025
fa2bc36
Add changelog entry
daymxn Aug 13, 2025
8ab664c
Update tests using `createInstance`
daymxn Aug 13, 2025
860ee49
Use default params when initializing config
daymxn Aug 13, 2025
187de4f
formatting
daymxn Aug 13, 2025
10f445c
manual formatting
daymxn Aug 13, 2025
8ce15d2
Fix broken link in unit tests
daymxn Aug 13, 2025
ce0da47
Add missing default arg on GenerativeModel for config
daymxn Aug 13, 2025
9a9a1c5
Update FirebaseAI/CHANGELOG.md
daymxn Aug 13, 2025
446f62d
Add error message
daymxn Aug 13, 2025
61c5ad6
formatting
daymxn Aug 13, 2025
4f84809
Merge branch 'daymxn-fal-limitedusetokens-support' of github.com:fire…
daymxn Aug 13, 2025
9c551c7
Update domain to be more adaptive
daymxn Aug 13, 2025
cc578bd
Update AppCheckOptions.swift
daymxn Aug 13, 2025
604ec4c
Update AppCheckOptions.swift
daymxn Aug 13, 2025
9ef2511
Update AppCheckOptions.swift
daymxn Aug 13, 2025
0857ffa
Update AppCheckOptions.swift
daymxn Aug 13, 2025
d3e31eb
Update AppCheckOptions.swift
daymxn Aug 13, 2025
df24a84
Update FirebaseAI/Sources/AppCheckOptions.swift
daymxn Aug 13, 2025
3e6c17d
Migrate to parameter instead of config struct
daymxn Aug 18, 2025
0a0976b
Update CHANGELOG.md
daymxn Aug 18, 2025
cb0d3c8
formatting
daymxn Aug 18, 2025
17113f3
Remove trailing whitespace
daymxn Aug 18, 2025
4e10e12
Merge branch 'main' into daymxn-fal-limitedusetokens-support
daymxn Aug 19, 2025
4774502
Fix left overs from config struct
daymxn Aug 19, 2025
e80fade
Make app check token result sendable
daymxn Aug 19, 2025
cd7312a
formatting
daymxn Aug 19, 2025
b5940dc
Fix missing param
daymxn Aug 19, 2025
4a8ea12
Add limited use tests
daymxn Aug 19, 2025
1dc7d7f
Update FirebaseAI.swift
daymxn Aug 19, 2025
19b4522
Update CHANGELOG.md
daymxn Aug 19, 2025
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
9 changes: 9 additions & 0 deletions FirebaseAI/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
# Unreleased
- [feature] Added a new configuration option to use limited-use App
Check tokens for attesting Firebase AI Logic requests. This enhances
security against replay attacks. To use this feature, configure it
explicitly via the new `useLimitedUseAppCheckTokens` parameter when
initializing `FirebaseAI`. We recommend migrating to limited-use
tokens now, so your app will be ready to take advantage of replay
protection when it becomes available for Firebase AI Logic.

# 12.2.0
- [feature] Added support for returning thought summaries, which are synthesized
versions of a model's internal reasoning process. (#15096)
Expand Down
57 changes: 49 additions & 8 deletions FirebaseAI/Sources/FirebaseAI.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,36 @@ public final class FirebaseAI: Sendable {
/// ``FirebaseApp``.
/// - backend: The backend API for the Firebase AI SDK; if not specified, uses the default
/// ``Backend/googleAI()`` (Gemini Developer API).
/// - config: Configuration options for the Firebase AI SDK that propogate across all models
/// created. Uses default options when not specified, see the ``FirebaseAI.Config``
/// documentation for more information.
/// - useLimitedUseAppCheckTokens: When sending tokens to the backend, this option enables
/// the usage of App Check's limited-use tokens instead of the standard cached tokens.
///
/// A new limited-use tokens will be generated for each request; providing a smaller attack
/// surface for malicious parties to hijack tokens. When used alongside replay protection,
/// limited-use tokens are also _consumed_ after each request, ensuring they can't be used
/// again.
///
/// _This flag is set to `false` by default._
///
/// > Important: Replay protection is not currently supported for the FirebaseAI backend.
/// > While this feature is being developed, you can still migrate to using
/// > limited-use tokens. Because limited-use tokens are backwards compatible, you can still
/// > use them without replay protection. Due to their shorter TTL over standard App Check
/// > tokens, they still provide a security benefit.
/// >
/// > Migrating to limited-use tokens sooner minimizes disruption when support for replay
/// > protection is added.
/// - Returns: A `FirebaseAI` instance, configured with the custom `FirebaseApp`.
public static func firebaseAI(app: FirebaseApp? = nil,
backend: Backend = .googleAI()) -> FirebaseAI {
backend: Backend = .googleAI(),
useLimitedUseAppCheckTokens: Bool = false) -> FirebaseAI {
let instance = createInstance(
app: app,
location: backend.location,
apiConfig: backend.apiConfig
apiConfig: backend.apiConfig,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
// Verify that the `FirebaseAI` instance is always configured with the production endpoint since
// this is the public API surface for creating an instance.
Expand Down Expand Up @@ -90,7 +113,8 @@ public final class FirebaseAI: Sendable {
tools: tools,
toolConfig: toolConfig,
systemInstruction: systemInstruction,
requestOptions: requestOptions
requestOptions: requestOptions,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
}

Expand Down Expand Up @@ -126,7 +150,8 @@ public final class FirebaseAI: Sendable {
apiConfig: apiConfig,
generationConfig: generationConfig,
safetySettings: safetySettings,
requestOptions: requestOptions
requestOptions: requestOptions,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
}

Expand All @@ -141,6 +166,8 @@ public final class FirebaseAI: Sendable {

let apiConfig: APIConfig

let useLimitedUseAppCheckTokens: Bool

/// A map of active `FirebaseAI` instances keyed by the `FirebaseApp` name and the `location`,
/// in the format `appName:location`.
private nonisolated(unsafe) static var instances: [InstanceKey: FirebaseAI] = [:]
Expand All @@ -156,7 +183,8 @@ public final class FirebaseAI: Sendable {
)

static func createInstance(app: FirebaseApp?, location: String?,
apiConfig: APIConfig) -> FirebaseAI {
apiConfig: APIConfig,
useLimitedUseAppCheckTokens: Bool) -> FirebaseAI {
guard let app = app ?? FirebaseApp.app() else {
fatalError("No instance of the default Firebase app was found.")
}
Expand All @@ -166,16 +194,27 @@ public final class FirebaseAI: Sendable {
// Unlock before the function returns.
defer { os_unfair_lock_unlock(&instancesLock) }

let instanceKey = InstanceKey(appName: app.name, location: location, apiConfig: apiConfig)
let instanceKey = InstanceKey(
appName: app.name,
location: location,
apiConfig: apiConfig,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
if let instance = instances[instanceKey] {
return instance
}
let newInstance = FirebaseAI(app: app, location: location, apiConfig: apiConfig)
let newInstance = FirebaseAI(
app: app,
location: location,
apiConfig: apiConfig,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
instances[instanceKey] = newInstance
return newInstance
}

init(app: FirebaseApp, location: String?, apiConfig: APIConfig) {
init(app: FirebaseApp, location: String?, apiConfig: APIConfig,
useLimitedUseAppCheckTokens: Bool) {
guard let projectID = app.options.projectID else {
fatalError("The Firebase app named \"\(app.name)\" has no project ID in its configuration.")
}
Expand All @@ -195,6 +234,7 @@ public final class FirebaseAI: Sendable {
)
self.apiConfig = apiConfig
self.location = location
self.useLimitedUseAppCheckTokens = useLimitedUseAppCheckTokens
}

func modelResourceName(modelName: String) -> String {
Expand Down Expand Up @@ -249,5 +289,6 @@ public final class FirebaseAI: Sendable {
let appName: String
let location: String?
let apiConfig: APIConfig
let useLimitedUseAppCheckTokens: Bool
}
}
54 changes: 52 additions & 2 deletions FirebaseAI/Sources/GenerativeAIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ struct GenerativeAIService {

private let urlSession: URLSession

init(firebaseInfo: FirebaseInfo, urlSession: URLSession) {
private let useLimitedUseAppCheckTokens: Bool

init(firebaseInfo: FirebaseInfo, urlSession: URLSession, useLimitedUseAppCheckTokens: Bool) {
self.firebaseInfo = firebaseInfo
self.urlSession = urlSession
self.useLimitedUseAppCheckTokens = useLimitedUseAppCheckTokens
}

func loadRequest<T: GenerativeAIRequest>(request: T) async throws -> T.Response {
Expand Down Expand Up @@ -177,7 +180,7 @@ struct GenerativeAIService {
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

if let appCheck = firebaseInfo.appCheck {
let tokenResult = await appCheck.getToken(forcingRefresh: false)
let tokenResult = try await fetchAppCheckToken(appCheck: appCheck)
urlRequest.setValue(tokenResult.token, forHTTPHeaderField: "X-Firebase-AppCheck")
if let error = tokenResult.error {
AILog.error(
Expand Down Expand Up @@ -207,6 +210,53 @@ struct GenerativeAIService {
return urlRequest
}

private func fetchAppCheckToken(appCheck: AppCheckInterop) async throws
-> FIRAppCheckTokenResultInterop {
if useLimitedUseAppCheckTokens {
if let token = await getLimitedUseAppCheckToken(appCheck: appCheck) {
return token
}

let errorMessage =
"The provided App Check token provider doesn't implement getLimitedUseToken(), but requireLimitedUseTokens was enabled."

#if Debug
fatalError(errorMessage)
#else
throw NSError(
domain: "\(Constants.baseErrorDomain).\(Self.self)",
code: AILog.MessageCode.appCheckTokenFetchFailed.rawValue,
userInfo: [NSLocalizedDescriptionKey: errorMessage]
)
#endif
}

return await appCheck.getToken(forcingRefresh: false)
}

private func getLimitedUseAppCheckToken(appCheck: AppCheckInterop) async
-> FIRAppCheckTokenResultInterop? {
// At the moment, `await` doesn’t get along with Objective-C’s optional protocol methods.
await withCheckedContinuation { (continuation: CheckedContinuation<
FIRAppCheckTokenResultInterop?,
Never
>) in
guard
useLimitedUseAppCheckTokens,
// `getLimitedUseToken(completion:)` is an optional protocol method. Optional binding
// is performed to make sure `continuation` is called even if the method’s not implemented.
let limitedUseTokenClosure = appCheck.getLimitedUseToken
else {
return continuation.resume(returning: nil)
}

limitedUseTokenClosure { tokenResult in
// The placeholder token should be used in the case of App Check error.
continuation.resume(returning: tokenResult)
}
}
}

private func httpResponse(urlResponse: URLResponse) throws -> HTTPURLResponse {
// The following condition should always be true: "Whenever you make HTTP URL load requests, any
// response objects you get back from the URLSession, NSURLConnection, or NSURLDownload class
Expand Down
8 changes: 6 additions & 2 deletions FirebaseAI/Sources/GenerativeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@ public final class GenerativeModel: Sendable {
/// only text content is supported.
/// - requestOptions: Configuration parameters for sending requests to the backend.
/// - urlSession: The `URLSession` to use for requests; defaults to `URLSession.shared`.
/// - useLimitedUseAppCheckTokens: Use App Check's limited-use tokens instead of the standard
/// cached tokens.
init(modelName: String,
modelResourceName: String,
firebaseInfo: FirebaseInfo,
Expand All @@ -86,13 +88,15 @@ public final class GenerativeModel: Sendable {
toolConfig: ToolConfig? = nil,
systemInstruction: ModelContent? = nil,
requestOptions: RequestOptions,
urlSession: URLSession = GenAIURLSession.default) {
urlSession: URLSession = GenAIURLSession.default,
useLimitedUseAppCheckTokens: Bool = false) {
self.modelName = modelName
self.modelResourceName = modelResourceName
self.apiConfig = apiConfig
generativeAIService = GenerativeAIService(
firebaseInfo: firebaseInfo,
urlSession: urlSession
urlSession: urlSession,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
self.generationConfig = generationConfig
self.safetySettings = safetySettings
Expand Down
6 changes: 4 additions & 2 deletions FirebaseAI/Sources/Types/Public/Imagen/ImagenModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,14 @@ public final class ImagenModel {
generationConfig: ImagenGenerationConfig?,
safetySettings: ImagenSafetySettings?,
requestOptions: RequestOptions,
urlSession: URLSession = GenAIURLSession.default) {
urlSession: URLSession = GenAIURLSession.default,
useLimitedUseAppCheckTokens: Bool = false) {
self.modelResourceName = modelResourceName
self.apiConfig = apiConfig
generativeAIService = GenerativeAIService(
firebaseInfo: firebaseInfo,
urlSession: urlSession
urlSession: urlSession,
useLimitedUseAppCheckTokens: useLimitedUseAppCheckTokens
)
self.generationConfig = generationConfig
self.safetySettings = safetySettings
Expand Down
6 changes: 4 additions & 2 deletions FirebaseAI/Tests/TestApp/Tests/Utilities/InstanceConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ extension FirebaseAI {
return FirebaseAI.createInstance(
app: instanceConfig.app,
location: location,
apiConfig: instanceConfig.apiConfig
apiConfig: instanceConfig.apiConfig,
useLimitedUseAppCheckTokens: false
)
case .googleAI:
assert(
Expand All @@ -131,7 +132,8 @@ extension FirebaseAI {
return FirebaseAI.createInstance(
app: instanceConfig.app,
location: nil,
apiConfig: instanceConfig.apiConfig
apiConfig: instanceConfig.apiConfig,
useLimitedUseAppCheckTokens: false
)
}
}
Expand Down
11 changes: 8 additions & 3 deletions FirebaseAI/Tests/Unit/Fakes/AppCheckInteropFake.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ class AppCheckInteropFake: NSObject, AppCheckInterop {
return AppCheckTokenResultInteropFake(token: token, error: error)
}

func getLimitedUseToken() async -> any FIRAppCheckTokenResultInterop {
return AppCheckTokenResultInteropFake(token: "limited_use_\(token)", error: error)
}

func tokenDidChangeNotificationName() -> String {
fatalError("\(#function) not implemented.")
}
Expand All @@ -52,9 +56,10 @@ class AppCheckInteropFake: NSObject, AppCheckInterop {
fatalError("\(#function) not implemented.")
}

private class AppCheckTokenResultInteropFake: NSObject, FIRAppCheckTokenResultInterop {
var token: String
var error: Error?
private class AppCheckTokenResultInteropFake: NSObject, FIRAppCheckTokenResultInterop,
@unchecked Sendable {
let token: String
let error: Error?

init(token: String, error: Error?) {
self.token = token
Expand Down
25 changes: 25 additions & 0 deletions FirebaseAI/Tests/Unit/GenerativeModelVertexAITests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,31 @@ final class GenerativeModelVertexAITests: XCTestCase {
_ = try await model.generateContent(testPrompt)
}

func testGenerateContent_appCheck_validToken_limitedUse() async throws {
let appCheckToken = "test-valid-token"
model = GenerativeModel(
modelName: testModelName,
modelResourceName: testModelResourceName,
firebaseInfo: GenerativeModelTestUtil.testFirebaseInfo(
appCheck: AppCheckInteropFake(token: appCheckToken)
),
apiConfig: apiConfig,
tools: nil,
requestOptions: RequestOptions(),
urlSession: urlSession,
useLimitedUseAppCheckTokens: true
)
MockURLProtocol
.requestHandler = try GenerativeModelTestUtil.httpRequestHandler(
forResource: "unary-success-basic-reply-short",
withExtension: "json",
subdirectory: vertexSubdirectory,
appCheckToken: "limited_use_\(appCheckToken)"
)

_ = try await model.generateContent(testPrompt)
}

func testGenerateContent_dataCollectionOff() async throws {
let appCheckToken = "test-valid-token"
model = GenerativeModel(
Expand Down
2 changes: 1 addition & 1 deletion FirebaseAI/Tests/Unit/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
See the Firebase AI SDK
[README](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseVertexAI#unit-tests)
[README](https://github.com/firebase/firebase-ios-sdk/tree/main/FirebaseAI#unit-tests)
for required setup instructions.
Loading
Loading