Skip to content

[Functions] Pass placeholder app check tokens on error #14467

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

Merged
merged 1 commit into from
Feb 19, 2025
Merged
Show file tree
Hide file tree
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
19 changes: 7 additions & 12 deletions FirebaseFunctions/Sources/Internal/FunctionsContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,9 @@ struct FunctionsContextProvider {
private func getAppCheckToken(options: HTTPSCallableOptions?) async -> String? {
guard
options?.requireLimitedUseAppCheckTokens != true,
let tokenResult = await appCheck?.getToken(forcingRefresh: false),
tokenResult.error == nil
let tokenResult = await appCheck?.getToken(forcingRefresh: false)
else { return nil }
// The placeholder token should be used in the case of App Check error.
return tokenResult.token
}

Expand All @@ -77,8 +77,7 @@ struct FunctionsContextProvider {
}

limitedUseTokenClosure { tokenResult in
// Make sure there’s no error and the token is valid:
guard tokenResult.error == nil else { return continuation.resume(returning: nil) }
// The placeholder token should be used in the case of App Check error.
continuation.resume(returning: tokenResult.token)
}
}
Expand Down Expand Up @@ -111,21 +110,17 @@ struct FunctionsContextProvider {
// If it’s not implemented, we still need to leave the dispatch group.
if let limitedUseTokenClosure = appCheck.getLimitedUseToken {
limitedUseTokenClosure { tokenResult in
// Send only valid token to functions.
if tokenResult.error == nil {
limitedUseAppCheckToken = tokenResult.token
}
// In the case of an error, the token will be the placeholder token.
limitedUseAppCheckToken = tokenResult.token
dispatchGroup.leave()
}
} else {
dispatchGroup.leave()
}
} else {
appCheck.getToken(forcingRefresh: false) { tokenResult in
// Send only valid token to functions.
if tokenResult.error == nil {
appCheckToken = tokenResult.token
}
// In the case of an error, the token will be the placeholder token.
appCheckToken = tokenResult.token
dispatchGroup.leave()
}
}
Expand Down
68 changes: 62 additions & 6 deletions FirebaseFunctions/Tests/Unit/ContextProviderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ class ContextProviderTests: XCTestCase {
code: -1,
userInfo: nil
))
let appCheckLimitedUseTokenError = FIRAppCheckTokenResultFake(token: "limited use token",
error: NSError(
domain: "testAppCheckError",
code: -1,
userInfo: nil
))
let appCheckTokenSuccess = FIRAppCheckTokenResultFake(token: "valid_token", error: nil)
let messagingFake = FIRMessagingInteropFake()

Expand Down Expand Up @@ -143,8 +149,20 @@ class ContextProviderTests: XCTestCase {

XCTAssertNil(context.authToken)
XCTAssertNil(context.fcmToken)
// Don't expect any token in the case of App Check error.
XCTAssertNil(context.appCheckToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.appCheckToken, appCheckFake.tokenResult.token)
}

func testAsyncContextWithAppCheckOnlyError_LimitedUseToken() async throws {
appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)

let context = try await provider.context(options: .init(requireLimitedUseAppCheckTokens: true))

XCTAssertNil(context.authToken)
XCTAssertNil(context.fcmToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.limitedUseAppCheckToken, appCheckFake.limitedUseTokenResult.token)
}

func testContextWithAppCheckOnlyError() {
Expand All @@ -156,8 +174,24 @@ class ContextProviderTests: XCTestCase {
XCTAssertNil(error)
XCTAssertNil(context.authToken)
XCTAssertNil(context.fcmToken)
// Don't expect any token in the case of App Check error.
XCTAssertNil(context.appCheckToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.appCheckToken, self.appCheckFake.tokenResult.token)
expectation.fulfill()
}
waitForExpectations(timeout: 0.1)
}

func testContextWithAppCheckOnlyError_LimitedUseToken() {
appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError
let provider = FunctionsContextProvider(auth: nil, messaging: nil, appCheck: appCheckFake)
let expectation = expectation(description: "Verify bad app check token")
provider.getContext(options: .init(requireLimitedUseAppCheckTokens: true)) { context, error in
XCTAssertNotNil(context)
XCTAssertNil(error)
XCTAssertNil(context.authToken)
XCTAssertNil(context.fcmToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.limitedUseAppCheckToken, self.appCheckFake.limitedUseTokenResult.token)
expectation.fulfill()
}
waitForExpectations(timeout: 0.1)
Expand Down Expand Up @@ -264,8 +298,30 @@ class ContextProviderTests: XCTestCase {
XCTAssertEqual(error as NSError?, authError)
XCTAssertNil(context.authToken)
XCTAssertEqual(context.fcmToken, self.messagingFake.fcmToken)
// Don't expect any token in the case of App Check error.
XCTAssertNil(context.appCheckToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.appCheckToken, self.appCheckFake.tokenResult.token)
expectation.fulfill()
}
waitForExpectations(timeout: 0.1)
}

func testAllContextsAuthAndAppCheckError_LimitedUseToken() {
appCheckFake.limitedUseTokenResult = appCheckLimitedUseTokenError
let authError = NSError(domain: "com.functions.tests", code: 4, userInfo: nil)
let auth = FIRAuthInteropFake(token: nil, userID: "userID", error: authError)
let provider = FunctionsContextProvider(
auth: auth,
messaging: messagingFake,
appCheck: appCheckFake
)
let expectation = expectation(description: "All contexts with errors")
provider.getContext(options: .init(requireLimitedUseAppCheckTokens: true)) { context, error in
XCTAssertNotNil(context)
XCTAssertEqual(error as NSError?, authError)
XCTAssertNil(context.authToken)
XCTAssertEqual(context.fcmToken, self.messagingFake.fcmToken)
// Expect placeholder token in the case of App Check error.
XCTAssertEqual(context.limitedUseAppCheckToken, self.appCheckFake.limitedUseTokenResult.token)
expectation.fulfill()
}
waitForExpectations(timeout: 0.1)
Expand Down
12 changes: 8 additions & 4 deletions FirebaseFunctions/Tests/Unit/FunctionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,18 +215,22 @@ class FunctionsTests: XCTestCase {
waitForExpectations(timeout: 1.5)
}

func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithoutToken() {
func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithPlaceholderToken() {
// Given
appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake(
token: "dummy token",
token: "limited use token",
error: NSError(domain: #function, code: -1)
)

let httpRequestExpectation = expectation(description: "HTTPRequestExpectation")
fetcherService.testBlock = { fetcherToTest, testResponse in
// Assert that header does not contain an AppCheck token.
fetcherToTest.request?.allHTTPHeaderFields?.forEach { key, _ in
XCTAssertNotEqual(key, "X-Firebase-AppCheck")
do {
let appCheckHeader = try XCTUnwrap(fetcherToTest.request?
.allHTTPHeaderFields?["X-Firebase-AppCheck"])
XCTAssertEqual(appCheckHeader, self.appCheckFake.limitedUseTokenResult.token)
} catch {
XCTFail("Unexpected failure: \(error)")
}

testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil)
Expand Down
Loading