From bb8df26fedc453b96fe55da42ec08200b9974fec Mon Sep 17 00:00:00 2001 From: Yvonne Cheng Date: Wed, 10 May 2023 12:46:07 -0700 Subject: [PATCH 01/31] draft pr for callable functions --- FirebaseAppCheck/Interop/FIRAppCheckInterop.h | 4 + FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 15 ++ FirebaseFunctions/Sources/Functions.swift | 45 +++++- .../Sources/Internal/FunctionsContext.swift | 21 ++- .../Tests/Integration/IntegrationTests.swift | 149 ++++++++++-------- .../Tests/ObjCIntegration/ObjCAPITests.m | 4 +- .../Tests/ObjCIntegration/ObjCPPAPITests.mm | 5 +- 7 files changed, 165 insertions(+), 78 deletions(-) diff --git a/FirebaseAppCheck/Interop/FIRAppCheckInterop.h b/FirebaseAppCheck/Interop/FIRAppCheckInterop.h index c08f729b6b4..adf241d08e1 100644 --- a/FirebaseAppCheck/Interop/FIRAppCheckInterop.h +++ b/FirebaseAppCheck/Interop/FIRAppCheckInterop.h @@ -31,6 +31,10 @@ NS_SWIFT_NAME(AppCheckInterop) @protocol FIRAppCheckInterop completion:(FIRAppCheckTokenHandlerInterop)handler NS_SWIFT_NAME(getToken(forcingRefresh:completion:)); +/// Retrieve a new limited-use Firebase App Check token +- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler + NS_SWIFT_NAME(getLimitedUseToken(completion:)); + /// A notification with the specified name is sent to the default notification center /// (`NotificationCenter.default`) each time a Firebase app check token is refreshed. /// The user info dictionary contains `-[self notificationTokenKey]` and diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 2c4f2962d97..02b10bb1be3 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -234,6 +234,21 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh }); } +- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler { + [self retrieveLimitedUseToken] + .then(^id _Nullable(FIRAppCheckToken *token) { + FIRAppCheckTokenResult *result = [[FIRAppCheckTokenResult alloc] initWithToken:token.token + error:nil]; + handler(result); + return result; + }) + .catch(^(NSError *_Nonnull error) { + FIRAppCheckTokenResult *result = + [[FIRAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error]; + handler(result); + }); +} + - (nonnull NSString *)tokenDidChangeNotificationName { return FIRAppCheckAppCheckTokenDidChangeNotification; } diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 02c914d220b..b30c1046e3b 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -59,6 +59,9 @@ internal enum FunctionsConstants { /// The region to use for all function references. internal let region: String + /// The boolean to decide if getLimitedUseToken() is generated + internal var useLimitedUseAppCheckToken: Bool = false + // MARK: - Public APIs /** @@ -139,11 +142,19 @@ internal enum FunctionsConstants { * Creates a reference to the Callable HTTPS trigger with the given name. * - Parameter name The name of the Callable HTTPS trigger. */ - @objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable { + @objc(HTTPSCallableWithName:useLimitedUseAppCheckToken:) open func httpsCallable(_ name: String, + useLimitedUseAppCheckToken: Bool = + false) + -> HTTPSCallable { + self.useLimitedUseAppCheckToken = useLimitedUseAppCheckToken return HTTPSCallable(functions: self, name: name) } - @objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable { + @objc(HTTPSCallableWithURL:useLimitedUseAppCheckToken:) open func httpsCallable(_ url: URL, + useLimitedUseAppCheckToken: Bool = + false) + -> HTTPSCallable { + self.useLimitedUseAppCheckToken = useLimitedUseAppCheckToken return HTTPSCallable(functions: self, url: url) } @@ -157,6 +168,7 @@ internal enum FunctionsConstants { /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ name: String, + useLimitedUseAppCheckToken: Bool, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, encoder: FirebaseDataEncoder = FirebaseDataEncoder( @@ -164,7 +176,11 @@ internal enum FunctionsConstants { decoder: FirebaseDataDecoder = FirebaseDataDecoder( )) -> Callable { - return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder) + return Callable( + callable: httpsCallable(name, useLimitedUseAppCheckToken: useLimitedUseAppCheckToken), + encoder: encoder, + decoder: decoder + ) } /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` @@ -177,6 +193,7 @@ internal enum FunctionsConstants { /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ url: URL, + useLimitedUseAppCheckToken: Bool, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, encoder: FirebaseDataEncoder = FirebaseDataEncoder( @@ -184,7 +201,11 @@ internal enum FunctionsConstants { decoder: FirebaseDataDecoder = FirebaseDataDecoder( )) -> Callable { - return Callable(callable: httpsCallable(url), encoder: encoder, decoder: decoder) + return Callable( + callable: httpsCallable(url, useLimitedUseAppCheckToken: useLimitedUseAppCheckToken), + encoder: encoder, + decoder: decoder + ) } /** @@ -353,8 +374,20 @@ internal enum FunctionsConstants { fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader) } - if let appCheckToken = context.appCheckToken { - fetcher.setRequestValue(appCheckToken, forHTTPHeaderField: Constants.appCheckTokenHeader) + if useLimitedUseAppCheckToken == true { + if let appCheckToken = context.limitedUseAppCheckToken { + fetcher.setRequestValue( + appCheckToken, + forHTTPHeaderField: Constants.appCheckTokenHeader + ) + } + } else { + if let appCheckToken = context.appCheckToken { + fetcher.setRequestValue( + appCheckToken, + forHTTPHeaderField: Constants.appCheckTokenHeader + ) + } } // Override normal security rules if this is a local test. diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 06f0ba91b9f..8dc20756183 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -22,11 +22,14 @@ internal class FunctionsContext: NSObject { let authToken: String? let fcmToken: String? let appCheckToken: String? + let limitedUseAppCheckToken: String? - init(authToken: String?, fcmToken: String?, appCheckToken: String?) { + init(authToken: String?, fcmToken: String?, appCheckToken: String?, + limitedUseAppCheckToken: String?) { self.authToken = authToken self.fcmToken = fcmToken self.appCheckToken = appCheckToken + self.limitedUseAppCheckToken = limitedUseAppCheckToken } } @@ -54,6 +57,7 @@ internal class FunctionsContextProvider: NSObject { var authToken: String? var appCheckToken: String? var error: Error? + var limitedUseAppCheckToken: String? if let auth = auth { dispatchGroup.enter() @@ -77,10 +81,23 @@ internal class FunctionsContextProvider: NSObject { } } + if let appCheck = appCheck { + dispatchGroup.enter() + + appCheck.getLimitedUseToken { tokenResult in + // Send only valid token to functions. + if tokenResult.error == nil { + appCheckToken = tokenResult.token + } + dispatchGroup.leave() + } + } + dispatchGroup.notify(queue: .main) { let context = FunctionsContext(authToken: authToken, fcmToken: self.messaging?.fcmToken, - appCheckToken: appCheckToken) + appCheckToken: appCheckToken, + limitedUseAppCheckToken: limitedUseAppCheckToken) completion(context, error) } } diff --git a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift index 089eafd926d..77831df4c98 100644 --- a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift +++ b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift @@ -87,12 +87,15 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName = functions.httpsCallable("dataTest", + let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable(emulatorURL("dataTest"), - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self) + let byURL = functions.httpsCallable( + emulatorURL("dataTest"), + useLimitedUseAppCheckToken: false, + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self + ) for function in [byName, byURL] { let expectation = expectation(description: #function) @@ -126,12 +129,15 @@ class IntegrationTests: XCTestCase { null: nil ) - let byName = functions.httpsCallable("dataTest", + let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byUrl = functions.httpsCallable(emulatorURL("dataTest"), - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self) + let byUrl = functions.httpsCallable( + emulatorURL("dataTest"), + useLimitedUseAppCheckToken: false, + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self + ) for function in [byName, byUrl] { let response = try await function.call(data) @@ -147,12 +153,12 @@ class IntegrationTests: XCTestCase { func testScalar() { let byName = functions.httpsCallable( - "scalarTest", + "scalarTest", useLimitedUseAppCheckToken: false, requestAs: Int16.self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("scalarTest"), + emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false, requestAs: Int16.self, responseAs: Int.self ) @@ -175,12 +181,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsync() async throws { let byName = functions.httpsCallable( - "scalarTest", + "scalarTest", useLimitedUseAppCheckToken: false, requestAs: Int16.self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("scalarTest"), + emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false, requestAs: Int16.self, responseAs: Int.self ) @@ -193,8 +199,10 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsyncAlternateSignature() async throws { - let byName: Callable = functions.httpsCallable("scalarTest") - let byURL: Callable = functions.httpsCallable(emulatorURL("scalarTest")) + let byName: Callable = functions + .httpsCallable("scalarTest", useLimitedUseAppCheckToken: false) + let byURL: Callable = functions + .httpsCallable(emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false) for function in [byName, byURL] { let result = try await function.call(17) XCTAssertEqual(result, 76) @@ -216,12 +224,12 @@ class IntegrationTests: XCTestCase { functions.useEmulator(withHost: "localhost", port: 5005) let byName = functions.httpsCallable( - "tokenTest", + "tokenTest", useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("tokenTest"), + emulatorURL("tokenTest"), useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -256,12 +264,12 @@ class IntegrationTests: XCTestCase { functions.useEmulator(withHost: "localhost", port: 5005) let byName = functions.httpsCallable( - "tokenTest", + "tokenTest", useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("tokenTest"), + emulatorURL("tokenTest"), useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -275,12 +283,12 @@ class IntegrationTests: XCTestCase { func testFCMToken() { let byName = functions.httpsCallable( - "FCMTokenTest", + "FCMTokenTest", useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("FCMTokenTest"), + emulatorURL("FCMTokenTest"), useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -303,12 +311,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testFCMTokenAsync() async throws { let byName = functions.httpsCallable( - "FCMTokenTest", + "FCMTokenTest", useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("FCMTokenTest"), + emulatorURL("FCMTokenTest"), useLimitedUseAppCheckToken: false, requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -322,12 +330,12 @@ class IntegrationTests: XCTestCase { func testNull() { let byName = functions.httpsCallable( - "nullTest", + "nullTest", useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("nullTest"), + emulatorURL("nullTest"), useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) @@ -350,12 +358,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testNullAsync() async throws { let byName = functions.httpsCallable( - "nullTest", + "nullTest", useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("nullTest"), + emulatorURL("nullTest"), useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) @@ -369,12 +377,12 @@ class IntegrationTests: XCTestCase { func testMissingResult() { let byName = functions.httpsCallable( - "missingResultTest", + "missingResultTest", useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("missingResultTest"), + emulatorURL("missingResultTest"), useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) @@ -401,12 +409,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testMissingResultAsync() async { let byName = functions.httpsCallable( - "missingResultTest", + "missingResultTest", useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("missingResultTest"), + emulatorURL("missingResultTest"), useLimitedUseAppCheckToken: false, requestAs: Int?.self, responseAs: Int?.self ) @@ -425,12 +433,12 @@ class IntegrationTests: XCTestCase { func testUnhandledError() { let byName = functions.httpsCallable( - "unhandledErrorTest", + "unhandledErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unhandledErrorTest"), + emulatorURL("unhandledErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -457,12 +465,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnhandledErrorAsync() async { let byName = functions.httpsCallable( - "unhandledErrorTest", + "unhandledErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - "unhandledErrorTest", + "unhandledErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -481,12 +489,12 @@ class IntegrationTests: XCTestCase { func testUnknownError() { let byName = functions.httpsCallable( - "unknownErrorTest", + "unknownErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unknownErrorTest"), + emulatorURL("unknownErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -512,12 +520,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnknownErrorAsync() async { let byName = functions.httpsCallable( - "unknownErrorTest", + "unknownErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unknownErrorTest"), + emulatorURL("unknownErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -536,12 +544,12 @@ class IntegrationTests: XCTestCase { func testExplicitError() { let byName = functions.httpsCallable( - "explicitErrorTest", + "explicitErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - "explicitErrorTest", + "explicitErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -569,12 +577,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testExplicitErrorAsync() async { let byName = functions.httpsCallable( - "explicitErrorTest", + "explicitErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("explicitErrorTest"), + emulatorURL("explicitErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -595,12 +603,12 @@ class IntegrationTests: XCTestCase { func testHttpError() { let byName = functions.httpsCallable( - "httpErrorTest", + "httpErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("httpErrorTest"), + emulatorURL("httpErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -626,12 +634,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testHttpErrorAsync() async { let byName = functions.httpsCallable( - "httpErrorTest", + "httpErrorTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("httpErrorTest"), + emulatorURL("httpErrorTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -649,12 +657,12 @@ class IntegrationTests: XCTestCase { func testThrowError() { let byName = functions.httpsCallable( - "throwTest", + "throwTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("throwTest"), + emulatorURL("throwTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -681,12 +689,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testThrowErrorAsync() async { let byName = functions.httpsCallable( - "throwTest", + "throwTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("throwTest"), + emulatorURL("throwTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -705,12 +713,12 @@ class IntegrationTests: XCTestCase { func testTimeout() { let byName = functions.httpsCallable( - "timeoutTest", + "timeoutTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("timeoutTest"), + emulatorURL("timeoutTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -738,13 +746,13 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testTimeoutAsync() async { var byName = functions.httpsCallable( - "timeoutTest", + "timeoutTest", useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) byName.timeoutInterval = 0.05 var byURL = functions.httpsCallable( - emulatorURL("timeoutTest"), + emulatorURL("timeoutTest"), useLimitedUseAppCheckToken: false, requestAs: [Int].self, responseAs: Int.self ) @@ -772,12 +780,15 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName = functions.httpsCallable("dataTest", + let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable(emulatorURL("dataTest"), - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self) + let byURL = functions.httpsCallable( + emulatorURL("dataTest"), + useLimitedUseAppCheckToken: false, + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self + ) for function in [byName, byURL] { let expectation = expectation(description: #function) function(data) { result in @@ -810,13 +821,16 @@ class IntegrationTests: XCTestCase { null: nil ) - let byName = functions.httpsCallable("dataTest", + let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable(emulatorURL("dataTest"), - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self) + let byURL = functions.httpsCallable( + emulatorURL("dataTest"), + useLimitedUseAppCheckToken: false, + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self + ) for function in [byName, byURL] { let response = try await function(data) @@ -839,9 +853,12 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName: Callable = functions.httpsCallable("dataTest") + let byName: Callable = functions.httpsCallable( + "dataTest", + useLimitedUseAppCheckToken: false + ) let byURL: Callable = functions - .httpsCallable(emulatorURL("dataTest")) + .httpsCallable(emulatorURL("dataTest"), useLimitedUseAppCheckToken: false) for function in [byName, byURL] { let expectation = expectation(description: #function) @@ -876,9 +893,9 @@ class IntegrationTests: XCTestCase { ) let byName: Callable = functions - .httpsCallable("dataTest") + .httpsCallable("dataTest", useLimitedUseAppCheckToken: false) let byURL: Callable = functions - .httpsCallable(emulatorURL("dataTest")) + .httpsCallable(emulatorURL("dataTest"), useLimitedUseAppCheckToken: false) for function in [byName, byURL] { let response = try await function(data) diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m index 8ea86ccd1ed..f7234649599 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m @@ -34,8 +34,8 @@ - (void)apis { func = [FIRFunctions functionsForApp:app customDomain:@"my-domain"]; NSURL *url = [NSURL URLWithString:@"http://localhost:5050/project/location/name"]; - FIRHTTPSCallable *callable = [func HTTPSCallableWithURL:url]; - callable = [func HTTPSCallableWithName:@"name"]; + FIRHTTPSCallable *callable = [func HTTPSCallableWithURL:url useLimitedUseAppCheckToken:false]; + callable = [func HTTPSCallableWithName:@"name" useLimitedUseAppCheckToken:false]; [func useEmulatorWithHost:@"host" port:123]; diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm index 0c621c5242c..9265050f5c7 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm @@ -33,9 +33,10 @@ - (void)apis { func = [FIRFunctions functionsForApp:app region:@"my-region"]; func = [FIRFunctions functionsForApp:app customDomain:@"my-domain"]; - FIRHTTPSCallable *callable = [func HTTPSCallableWithName:@"name"]; + FIRHTTPSCallable *callable = [func HTTPSCallableWithName:@"name" + useLimitedUseAppCheckToken:false]; NSURL *url = [NSURL URLWithString:@"http://host:123/project/location/name"]; - callable = [func HTTPSCallableWithURL:url]; + callable = [func HTTPSCallableWithURL:url useLimitedUseAppCheckToken:false]; [func useEmulatorWithHost:@"host" port:123]; From 1e9b9898495ec3eb209a03869611ddb5117a2b1f Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:28:27 -0400 Subject: [PATCH 02/31] Adjust Functions podspec dependency on AppCheckInterop --- FirebaseFunctions.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions.podspec b/FirebaseFunctions.podspec index cb7af73e72b..06b82aaaf5d 100644 --- a/FirebaseFunctions.podspec +++ b/FirebaseFunctions.podspec @@ -39,7 +39,7 @@ Cloud Functions for Firebase. s.dependency 'FirebaseCore', '~> 10.0' s.dependency 'FirebaseCoreExtension', '~> 10.0' - s.dependency 'FirebaseAppCheckInterop', '~> 10.0' + s.dependency 'FirebaseAppCheckInterop', '~> 10.10' s.dependency 'FirebaseAuthInterop', '~> 10.0' s.dependency 'FirebaseMessagingInterop', '~> 10.0' s.dependency 'FirebaseSharedSwift', '~> 10.0' From 3fc0cce62587a1296adb8515e9b6d1a891ada51e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:30:33 -0400 Subject: [PATCH 03/31] Add 'HTTPSCallableOptions' class --- .../Sources/HTTPSCallableOptions.swift | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 FirebaseFunctions/Sources/HTTPSCallableOptions.swift diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift new file mode 100644 index 00000000000..7734ecd7c4d --- /dev/null +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -0,0 +1,28 @@ +// Copyright 2023 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 + +/// Configuration options for a ``HTTPSCallable`` instance. +@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject { + /// Whether or not to protect the callable function with a limited use App Check token. + @objc public let limitedUseAppCheckTokens: Bool + + /// Designated intializer. + /// - Parameter limitedUseAppCheckTokens: A boolean used to decide whether or not to + /// protect the callable function with a limited use App Check token. + @objc public init(limitedUseAppCheckTokens: Bool) { + self.limitedUseAppCheckTokens = limitedUseAppCheckTokens + } +} From aa7d457c647c6a2a6e5861d250b8cb4ca61a4b99 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:31:24 -0400 Subject: [PATCH 04/31] Re-implement with new HTTPSCallableOptions type --- FirebaseFunctions/Sources/Functions.swift | 88 ++++++++++++------- FirebaseFunctions/Sources/HTTPSCallable.swift | 10 ++- 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index b30c1046e3b..5dcac3d3252 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -59,9 +59,6 @@ internal enum FunctionsConstants { /// The region to use for all function references. internal let region: String - /// The boolean to decide if getLimitedUseToken() is generated - internal var useLimitedUseAppCheckToken: Bool = false - // MARK: - Public APIs /** @@ -138,37 +135,57 @@ internal enum FunctionsConstants { return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: customDomain) } - /** - * Creates a reference to the Callable HTTPS trigger with the given name. - * - Parameter name The name of the Callable HTTPS trigger. - */ - @objc(HTTPSCallableWithName:useLimitedUseAppCheckToken:) open func httpsCallable(_ name: String, - useLimitedUseAppCheckToken: Bool = - false) - -> HTTPSCallable { - self.useLimitedUseAppCheckToken = useLimitedUseAppCheckToken + /// Creates a reference to the Callable HTTPS trigger with the given name. + /// - Parameter name: The name of the Callable HTTPS trigger. + /// - Returns: A reference to a Callable HTTPS trigger. + @objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable { return HTTPSCallable(functions: self, name: name) } - @objc(HTTPSCallableWithURL:useLimitedUseAppCheckToken:) open func httpsCallable(_ url: URL, - useLimitedUseAppCheckToken: Bool = - false) - -> HTTPSCallable { - self.useLimitedUseAppCheckToken = useLimitedUseAppCheckToken + /// Creates a reference to the Callable HTTPS trigger with the given name and configuration options. + /// - Parameters: + /// - name: The name of the Callable HTTPS trigger. + /// - options: The options with which to customize the Callable HTTPS trigger. + /// - Returns: A reference to a Callable HTTPS trigger. + @objc(HTTPSCallableWithName:options:) public func httpsCallable( + _ name: String, + options: HTTPSCallableOptions + ) -> HTTPSCallable { + return HTTPSCallable(functions: self, name: name, options: options) + } + + /// Creates a reference to the Callable HTTPS trigger with the given name. + /// - Parameter url: The URL of the Callable HTTPS trigger. + /// - Returns: A reference to a Callable HTTPS trigger. + @objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable { return HTTPSCallable(functions: self, url: url) } + /// Creates a reference to the Callable HTTPS trigger with the given name and configuration options. + /// - Parameters: + /// - url: The URL of the Callable HTTPS trigger. + /// - options: The options with which to customize the Callable HTTPS trigger. + /// - Returns: A reference to a Callable HTTPS trigger. + @objc(HTTPSCallableWithURL:options:) public func httpsCallable( + _ url: URL, + options: HTTPSCallableOptions + ) -> HTTPSCallable { + return HTTPSCallable(functions: self, url: url, options: options) + } + /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` /// request and the type of a `Decodable` response. - /// - Parameter name: The name of the Callable HTTPS trigger - /// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable` - /// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - Parameter encoder: The encoder instance to use to run the encoding. - /// - Parameter decoder: The decoder instance to use to run the decoding. + /// - Parameters: + /// - name: The name of the Callable HTTPS trigger + /// - options: The options with which to customize the Callable HTTPS trigger. + /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` + /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` + /// - encoder: The encoder instance to use to run the encoding. + /// - decoder: The decoder instance to use to run the decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ name: String, - useLimitedUseAppCheckToken: Bool, + options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, encoder: FirebaseDataEncoder = FirebaseDataEncoder( @@ -177,7 +194,7 @@ internal enum FunctionsConstants { )) -> Callable { return Callable( - callable: httpsCallable(name, useLimitedUseAppCheckToken: useLimitedUseAppCheckToken), + callable: httpsCallable(name, options: options), encoder: encoder, decoder: decoder ) @@ -185,15 +202,17 @@ internal enum FunctionsConstants { /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` /// request and the type of a `Decodable` response. - /// - Parameter url: The url of the Callable HTTPS trigger - /// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable` - /// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - Parameter encoder: The encoder instance to use to run the encoding. - /// - Parameter decoder: The decoder instance to use to run the decoding. + /// - Parameters: + /// - url: The url of the Callable HTTPS trigger + /// - options: The options with which to customize the Callable HTTPS trigger. + /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` + /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` + /// - encoder: The encoder instance to use to run the encoding. + /// - decoder: The decoder instance to use to run the decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ url: URL, - useLimitedUseAppCheckToken: Bool, + options: HTTPSCallableOptions, requestAs: Request.Type = Request.self, responseAs: Response.Type = Response.self, encoder: FirebaseDataEncoder = FirebaseDataEncoder( @@ -202,7 +221,7 @@ internal enum FunctionsConstants { )) -> Callable { return Callable( - callable: httpsCallable(url, useLimitedUseAppCheckToken: useLimitedUseAppCheckToken), + callable: httpsCallable(url, options: options), encoder: encoder, decoder: decoder ) @@ -294,6 +313,7 @@ internal enum FunctionsConstants { internal func callFunction(name: String, withObject data: Any?, + options: HTTPSCallableOptions?, timeout: TimeInterval, completion: @escaping ((Result) -> Void)) { // Get context first. @@ -306,6 +326,7 @@ internal enum FunctionsConstants { let url = self.urlWithName(name) self.callFunction(url: URL(string: url)!, withObject: data, + options: options, timeout: timeout, context: context, completion: completion) @@ -315,6 +336,7 @@ internal enum FunctionsConstants { internal func callFunction(url: URL, withObject data: Any?, + options: HTTPSCallableOptions?, timeout: TimeInterval, completion: @escaping ((Result) -> Void)) { // Get context first. @@ -326,6 +348,7 @@ internal enum FunctionsConstants { } else { self.callFunction(url: url, withObject: data, + options: options, timeout: timeout, context: context, completion: completion) @@ -335,6 +358,7 @@ internal enum FunctionsConstants { private func callFunction(url: URL, withObject data: Any?, + options: HTTPSCallableOptions?, timeout: TimeInterval, context: FunctionsContext, completion: @escaping ((Result) -> Void)) { @@ -374,7 +398,7 @@ internal enum FunctionsConstants { fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader) } - if useLimitedUseAppCheckToken == true { + if options?.limitedUseAppCheckTokens == true { if let appCheckToken = context.limitedUseAppCheckToken { fetcher.setRequestValue( appCheckToken, diff --git a/FirebaseFunctions/Sources/HTTPSCallable.swift b/FirebaseFunctions/Sources/HTTPSCallable.swift index 2191d705e03..98ee918f685 100644 --- a/FirebaseFunctions/Sources/HTTPSCallable.swift +++ b/FirebaseFunctions/Sources/HTTPSCallable.swift @@ -50,6 +50,8 @@ open class HTTPSCallable: NSObject { private let endpoint: EndpointType + private let options: HTTPSCallableOptions? + // MARK: - Public Properties /** @@ -57,13 +59,15 @@ open class HTTPSCallable: NSObject { */ @objc open var timeoutInterval: TimeInterval = 70 - internal init(functions: Functions, name: String) { + internal init(functions: Functions, name: String, options: HTTPSCallableOptions? = nil) { self.functions = functions + self.options = options endpoint = .name(name) } - internal init(functions: Functions, url: URL) { + internal init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) { self.functions = functions + self.options = options endpoint = .url(url) } @@ -105,11 +109,13 @@ open class HTTPSCallable: NSObject { case let .name(name): functions.callFunction(name: name, withObject: data, + options: options, timeout: timeoutInterval, completion: callback) case let .url(url): functions.callFunction(url: url, withObject: data, + options: options, timeout: timeoutInterval, completion: callback) } From 893f74e9bf84e24ec49d4ec70218df83fc043fa8 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:34:10 -0400 Subject: [PATCH 05/31] Revert and update Objective-C API tests --- .../Tests/ObjCIntegration/ObjCAPITests.m | 10 ++++++++-- .../Tests/ObjCIntegration/ObjCPPAPITests.mm | 11 ++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m index f7234649599..a956ae80693 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m @@ -34,8 +34,14 @@ - (void)apis { func = [FIRFunctions functionsForApp:app customDomain:@"my-domain"]; NSURL *url = [NSURL URLWithString:@"http://localhost:5050/project/location/name"]; - FIRHTTPSCallable *callable = [func HTTPSCallableWithURL:url useLimitedUseAppCheckToken:false]; - callable = [func HTTPSCallableWithName:@"name" useLimitedUseAppCheckToken:false]; + FIRHTTPSCallable *callable = [func HTTPSCallableWithURL:url]; + callable = [func HTTPSCallableWithName:@"name"]; + + FIRHTTPSCallableOptions *options = + [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; + __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; + callable = [func HTTPSCallableWithURL:url options:options]; + callable = [func HTTPSCallableWithName:@"name" options:options]; [func useEmulatorWithHost:@"host" port:123]; diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm index 9265050f5c7..ae1ad5a1524 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm @@ -33,10 +33,15 @@ - (void)apis { func = [FIRFunctions functionsForApp:app region:@"my-region"]; func = [FIRFunctions functionsForApp:app customDomain:@"my-domain"]; - FIRHTTPSCallable *callable = [func HTTPSCallableWithName:@"name" - useLimitedUseAppCheckToken:false]; + FIRHTTPSCallable *callable = [func HTTPSCallableWithName:@"name"]; NSURL *url = [NSURL URLWithString:@"http://host:123/project/location/name"]; - callable = [func HTTPSCallableWithURL:url useLimitedUseAppCheckToken:false]; + callable = [func HTTPSCallableWithURL:url]; + + FIRHTTPSCallableOptions *options = + [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; + __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; + callable = [func HTTPSCallableWithURL:url options:options]; + callable = [func HTTPSCallableWithName:@"name" options:options]; [func useEmulatorWithHost:@"host" port:123]; From 765534de82c48c2dc448902705f54ca585d595a2 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:52:21 -0400 Subject: [PATCH 06/31] Mark new AppCheckInterop API optional for backwards compatibility --- FirebaseAppCheck/Interop/FIRAppCheckInterop.h | 12 ++++++++---- .../Sources/Internal/FunctionsContext.swift | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/FirebaseAppCheck/Interop/FIRAppCheckInterop.h b/FirebaseAppCheck/Interop/FIRAppCheckInterop.h index adf241d08e1..11ade09985c 100644 --- a/FirebaseAppCheck/Interop/FIRAppCheckInterop.h +++ b/FirebaseAppCheck/Interop/FIRAppCheckInterop.h @@ -31,10 +31,6 @@ NS_SWIFT_NAME(AppCheckInterop) @protocol FIRAppCheckInterop completion:(FIRAppCheckTokenHandlerInterop)handler NS_SWIFT_NAME(getToken(forcingRefresh:completion:)); -/// Retrieve a new limited-use Firebase App Check token -- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler - NS_SWIFT_NAME(getLimitedUseToken(completion:)); - /// A notification with the specified name is sent to the default notification center /// (`NotificationCenter.default`) each time a Firebase app check token is refreshed. /// The user info dictionary contains `-[self notificationTokenKey]` and @@ -47,6 +43,14 @@ NS_SWIFT_NAME(AppCheckInterop) @protocol FIRAppCheckInterop /// `tokenDidChangeNotificationName`. - (NSString *)notificationAppNameKey; +// MARK: - Optional API + +@optional + +/// Retrieve a new limited-use Firebase App Check token +- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler + NS_SWIFT_NAME(getLimitedUseToken(completion:)); + @end NS_ASSUME_NONNULL_END diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 8dc20756183..9852996f005 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -84,7 +84,7 @@ internal class FunctionsContextProvider: NSObject { if let appCheck = appCheck { dispatchGroup.enter() - appCheck.getLimitedUseToken { tokenResult in + appCheck.getLimitedUseToken? { tokenResult in // Send only valid token to functions. if tokenResult.error == nil { appCheckToken = tokenResult.token From ad5d68bd597d449d909bc4d58ba2175d43f661c5 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 15:52:56 -0400 Subject: [PATCH 07/31] Preserve existing codable Functions API --- FirebaseFunctions/Sources/Functions.swift | 50 +++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 5dcac3d3252..dc2a4e2117d 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -173,6 +173,31 @@ internal enum FunctionsConstants { return HTTPSCallable(functions: self, url: url, options: options) } + /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` + /// request and the type of a `Decodable` response. + /// - Parameters: + /// - name: The name of the Callable HTTPS trigger + /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` + /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` + /// - encoder: The encoder instance to use to run the encoding. + /// - decoder: The decoder instance to use to run the decoding. + /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. + open func httpsCallable(_ name: String, + requestAs: Request.Type = Request.self, + responseAs: Response.Type = Response.self, + encoder: FirebaseDataEncoder = FirebaseDataEncoder( + ), + decoder: FirebaseDataDecoder = FirebaseDataDecoder( + )) + -> Callable { + return Callable( + callable: httpsCallable(name), + encoder: encoder, + decoder: decoder + ) + } + /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` /// request and the type of a `Decodable` response. /// - Parameters: @@ -200,6 +225,31 @@ internal enum FunctionsConstants { ) } + /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` + /// request and the type of a `Decodable` response. + /// - Parameters: + /// - url: The url of the Callable HTTPS trigger + /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` + /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` + /// - encoder: The encoder instance to use to run the encoding. + /// - decoder: The decoder instance to use to run the decoding. + /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. + open func httpsCallable(_ url: URL, + requestAs: Request.Type = Request.self, + responseAs: Response.Type = Response.self, + encoder: FirebaseDataEncoder = FirebaseDataEncoder( + ), + decoder: FirebaseDataDecoder = FirebaseDataDecoder( + )) + -> Callable { + return Callable( + callable: httpsCallable(url), + encoder: encoder, + decoder: decoder + ) + } + /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` /// request and the type of a `Decodable` response. /// - Parameters: From 67a49df6eb026d4090e1b9725bb00e0bcc85ac6e Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 16:23:44 -0400 Subject: [PATCH 08/31] Revert integration tests --- .../Tests/Integration/IntegrationTests.swift | 149 ++++++++---------- 1 file changed, 66 insertions(+), 83 deletions(-) diff --git a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift index 77831df4c98..089eafd926d 100644 --- a/FirebaseFunctions/Tests/Integration/IntegrationTests.swift +++ b/FirebaseFunctions/Tests/Integration/IntegrationTests.swift @@ -87,15 +87,12 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, + let byName = functions.httpsCallable("dataTest", requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable( - emulatorURL("dataTest"), - useLimitedUseAppCheckToken: false, - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self - ) + let byURL = functions.httpsCallable(emulatorURL("dataTest"), + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) for function in [byName, byURL] { let expectation = expectation(description: #function) @@ -129,15 +126,12 @@ class IntegrationTests: XCTestCase { null: nil ) - let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, + let byName = functions.httpsCallable("dataTest", requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byUrl = functions.httpsCallable( - emulatorURL("dataTest"), - useLimitedUseAppCheckToken: false, - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self - ) + let byUrl = functions.httpsCallable(emulatorURL("dataTest"), + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) for function in [byName, byUrl] { let response = try await function.call(data) @@ -153,12 +147,12 @@ class IntegrationTests: XCTestCase { func testScalar() { let byName = functions.httpsCallable( - "scalarTest", useLimitedUseAppCheckToken: false, + "scalarTest", requestAs: Int16.self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false, + emulatorURL("scalarTest"), requestAs: Int16.self, responseAs: Int.self ) @@ -181,12 +175,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsync() async throws { let byName = functions.httpsCallable( - "scalarTest", useLimitedUseAppCheckToken: false, + "scalarTest", requestAs: Int16.self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false, + emulatorURL("scalarTest"), requestAs: Int16.self, responseAs: Int.self ) @@ -199,10 +193,8 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testScalarAsyncAlternateSignature() async throws { - let byName: Callable = functions - .httpsCallable("scalarTest", useLimitedUseAppCheckToken: false) - let byURL: Callable = functions - .httpsCallable(emulatorURL("scalarTest"), useLimitedUseAppCheckToken: false) + let byName: Callable = functions.httpsCallable("scalarTest") + let byURL: Callable = functions.httpsCallable(emulatorURL("scalarTest")) for function in [byName, byURL] { let result = try await function.call(17) XCTAssertEqual(result, 76) @@ -224,12 +216,12 @@ class IntegrationTests: XCTestCase { functions.useEmulator(withHost: "localhost", port: 5005) let byName = functions.httpsCallable( - "tokenTest", useLimitedUseAppCheckToken: false, + "tokenTest", requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("tokenTest"), useLimitedUseAppCheckToken: false, + emulatorURL("tokenTest"), requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -264,12 +256,12 @@ class IntegrationTests: XCTestCase { functions.useEmulator(withHost: "localhost", port: 5005) let byName = functions.httpsCallable( - "tokenTest", useLimitedUseAppCheckToken: false, + "tokenTest", requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("tokenTest"), useLimitedUseAppCheckToken: false, + emulatorURL("tokenTest"), requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -283,12 +275,12 @@ class IntegrationTests: XCTestCase { func testFCMToken() { let byName = functions.httpsCallable( - "FCMTokenTest", useLimitedUseAppCheckToken: false, + "FCMTokenTest", requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("FCMTokenTest"), useLimitedUseAppCheckToken: false, + emulatorURL("FCMTokenTest"), requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -311,12 +303,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testFCMTokenAsync() async throws { let byName = functions.httpsCallable( - "FCMTokenTest", useLimitedUseAppCheckToken: false, + "FCMTokenTest", requestAs: [String: Int].self, responseAs: [String: Int].self ) let byURL = functions.httpsCallable( - emulatorURL("FCMTokenTest"), useLimitedUseAppCheckToken: false, + emulatorURL("FCMTokenTest"), requestAs: [String: Int].self, responseAs: [String: Int].self ) @@ -330,12 +322,12 @@ class IntegrationTests: XCTestCase { func testNull() { let byName = functions.httpsCallable( - "nullTest", useLimitedUseAppCheckToken: false, + "nullTest", requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("nullTest"), useLimitedUseAppCheckToken: false, + emulatorURL("nullTest"), requestAs: Int?.self, responseAs: Int?.self ) @@ -358,12 +350,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testNullAsync() async throws { let byName = functions.httpsCallable( - "nullTest", useLimitedUseAppCheckToken: false, + "nullTest", requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("nullTest"), useLimitedUseAppCheckToken: false, + emulatorURL("nullTest"), requestAs: Int?.self, responseAs: Int?.self ) @@ -377,12 +369,12 @@ class IntegrationTests: XCTestCase { func testMissingResult() { let byName = functions.httpsCallable( - "missingResultTest", useLimitedUseAppCheckToken: false, + "missingResultTest", requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("missingResultTest"), useLimitedUseAppCheckToken: false, + emulatorURL("missingResultTest"), requestAs: Int?.self, responseAs: Int?.self ) @@ -409,12 +401,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testMissingResultAsync() async { let byName = functions.httpsCallable( - "missingResultTest", useLimitedUseAppCheckToken: false, + "missingResultTest", requestAs: Int?.self, responseAs: Int?.self ) let byURL = functions.httpsCallable( - emulatorURL("missingResultTest"), useLimitedUseAppCheckToken: false, + emulatorURL("missingResultTest"), requestAs: Int?.self, responseAs: Int?.self ) @@ -433,12 +425,12 @@ class IntegrationTests: XCTestCase { func testUnhandledError() { let byName = functions.httpsCallable( - "unhandledErrorTest", useLimitedUseAppCheckToken: false, + "unhandledErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unhandledErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("unhandledErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -465,12 +457,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnhandledErrorAsync() async { let byName = functions.httpsCallable( - "unhandledErrorTest", useLimitedUseAppCheckToken: false, + "unhandledErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - "unhandledErrorTest", useLimitedUseAppCheckToken: false, + "unhandledErrorTest", requestAs: [Int].self, responseAs: Int.self ) @@ -489,12 +481,12 @@ class IntegrationTests: XCTestCase { func testUnknownError() { let byName = functions.httpsCallable( - "unknownErrorTest", useLimitedUseAppCheckToken: false, + "unknownErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unknownErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("unknownErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -520,12 +512,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testUnknownErrorAsync() async { let byName = functions.httpsCallable( - "unknownErrorTest", useLimitedUseAppCheckToken: false, + "unknownErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("unknownErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("unknownErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -544,12 +536,12 @@ class IntegrationTests: XCTestCase { func testExplicitError() { let byName = functions.httpsCallable( - "explicitErrorTest", useLimitedUseAppCheckToken: false, + "explicitErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - "explicitErrorTest", useLimitedUseAppCheckToken: false, + "explicitErrorTest", requestAs: [Int].self, responseAs: Int.self ) @@ -577,12 +569,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testExplicitErrorAsync() async { let byName = functions.httpsCallable( - "explicitErrorTest", useLimitedUseAppCheckToken: false, + "explicitErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("explicitErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("explicitErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -603,12 +595,12 @@ class IntegrationTests: XCTestCase { func testHttpError() { let byName = functions.httpsCallable( - "httpErrorTest", useLimitedUseAppCheckToken: false, + "httpErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("httpErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("httpErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -634,12 +626,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testHttpErrorAsync() async { let byName = functions.httpsCallable( - "httpErrorTest", useLimitedUseAppCheckToken: false, + "httpErrorTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("httpErrorTest"), useLimitedUseAppCheckToken: false, + emulatorURL("httpErrorTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -657,12 +649,12 @@ class IntegrationTests: XCTestCase { func testThrowError() { let byName = functions.httpsCallable( - "throwTest", useLimitedUseAppCheckToken: false, + "throwTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("throwTest"), useLimitedUseAppCheckToken: false, + emulatorURL("throwTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -689,12 +681,12 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testThrowErrorAsync() async { let byName = functions.httpsCallable( - "throwTest", useLimitedUseAppCheckToken: false, + "throwTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("throwTest"), useLimitedUseAppCheckToken: false, + emulatorURL("throwTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -713,12 +705,12 @@ class IntegrationTests: XCTestCase { func testTimeout() { let byName = functions.httpsCallable( - "timeoutTest", useLimitedUseAppCheckToken: false, + "timeoutTest", requestAs: [Int].self, responseAs: Int.self ) let byURL = functions.httpsCallable( - emulatorURL("timeoutTest"), useLimitedUseAppCheckToken: false, + emulatorURL("timeoutTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -746,13 +738,13 @@ class IntegrationTests: XCTestCase { @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *) func testTimeoutAsync() async { var byName = functions.httpsCallable( - "timeoutTest", useLimitedUseAppCheckToken: false, + "timeoutTest", requestAs: [Int].self, responseAs: Int.self ) byName.timeoutInterval = 0.05 var byURL = functions.httpsCallable( - emulatorURL("timeoutTest"), useLimitedUseAppCheckToken: false, + emulatorURL("timeoutTest"), requestAs: [Int].self, responseAs: Int.self ) @@ -780,15 +772,12 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, + let byName = functions.httpsCallable("dataTest", requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable( - emulatorURL("dataTest"), - useLimitedUseAppCheckToken: false, - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self - ) + let byURL = functions.httpsCallable(emulatorURL("dataTest"), + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) for function in [byName, byURL] { let expectation = expectation(description: #function) function(data) { result in @@ -821,16 +810,13 @@ class IntegrationTests: XCTestCase { null: nil ) - let byName = functions.httpsCallable("dataTest", useLimitedUseAppCheckToken: false, + let byName = functions.httpsCallable("dataTest", requestAs: DataTestRequest.self, responseAs: DataTestResponse.self) - let byURL = functions.httpsCallable( - emulatorURL("dataTest"), - useLimitedUseAppCheckToken: false, - requestAs: DataTestRequest.self, - responseAs: DataTestResponse.self - ) + let byURL = functions.httpsCallable(emulatorURL("dataTest"), + requestAs: DataTestRequest.self, + responseAs: DataTestResponse.self) for function in [byName, byURL] { let response = try await function(data) @@ -853,12 +839,9 @@ class IntegrationTests: XCTestCase { array: [5, 6], null: nil ) - let byName: Callable = functions.httpsCallable( - "dataTest", - useLimitedUseAppCheckToken: false - ) + let byName: Callable = functions.httpsCallable("dataTest") let byURL: Callable = functions - .httpsCallable(emulatorURL("dataTest"), useLimitedUseAppCheckToken: false) + .httpsCallable(emulatorURL("dataTest")) for function in [byName, byURL] { let expectation = expectation(description: #function) @@ -893,9 +876,9 @@ class IntegrationTests: XCTestCase { ) let byName: Callable = functions - .httpsCallable("dataTest", useLimitedUseAppCheckToken: false) + .httpsCallable("dataTest") let byURL: Callable = functions - .httpsCallable(emulatorURL("dataTest"), useLimitedUseAppCheckToken: false) + .httpsCallable(emulatorURL("dataTest")) for function in [byName, byURL] { let response = try await function(data) From 846eb229e87141cc3d07ad7b5ac9b0fe77a59e16 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 16:24:45 -0400 Subject: [PATCH 09/31] Style --- FirebaseFunctions/Sources/Functions.swift | 18 ++++++++---------- .../Sources/HTTPSCallableOptions.swift | 16 ++++++++-------- .../Tests/ObjCIntegration/ObjCAPITests.m | 2 +- .../Tests/ObjCIntegration/ObjCPPAPITests.mm | 2 +- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index dc2a4e2117d..aa6ba415926 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -147,11 +147,10 @@ internal enum FunctionsConstants { /// - name: The name of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithName:options:) public func httpsCallable( - _ name: String, - options: HTTPSCallableOptions - ) -> HTTPSCallable { - return HTTPSCallable(functions: self, name: name, options: options) + @objc(HTTPSCallableWithName:options:) public func httpsCallable(_ name: String, + options: HTTPSCallableOptions) + -> HTTPSCallable { + return HTTPSCallable(functions: self, name: name, options: options) } /// Creates a reference to the Callable HTTPS trigger with the given name. @@ -166,11 +165,10 @@ internal enum FunctionsConstants { /// - url: The URL of the Callable HTTPS trigger. /// - options: The options with which to customize the Callable HTTPS trigger. /// - Returns: A reference to a Callable HTTPS trigger. - @objc(HTTPSCallableWithURL:options:) public func httpsCallable( - _ url: URL, - options: HTTPSCallableOptions - ) -> HTTPSCallable { - return HTTPSCallable(functions: self, url: url, options: options) + @objc(HTTPSCallableWithURL:options:) public func httpsCallable(_ url: URL, + options: HTTPSCallableOptions) + -> HTTPSCallable { + return HTTPSCallable(functions: self, url: url, options: options) } /// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable` diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift index 7734ecd7c4d..26d8c4be258 100644 --- a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -16,13 +16,13 @@ import Foundation /// Configuration options for a ``HTTPSCallable`` instance. @objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject { - /// Whether or not to protect the callable function with a limited use App Check token. - @objc public let limitedUseAppCheckTokens: Bool + /// Whether or not to protect the callable function with a limited use App Check token. + @objc public let limitedUseAppCheckTokens: Bool - /// Designated intializer. - /// - Parameter limitedUseAppCheckTokens: A boolean used to decide whether or not to - /// protect the callable function with a limited use App Check token. - @objc public init(limitedUseAppCheckTokens: Bool) { - self.limitedUseAppCheckTokens = limitedUseAppCheckTokens - } + /// Designated intializer. + /// - Parameter limitedUseAppCheckTokens: A boolean used to decide whether or not to + /// protect the callable function with a limited use App Check token. + @objc public init(limitedUseAppCheckTokens: Bool) { + self.limitedUseAppCheckTokens = limitedUseAppCheckTokens + } } diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m index a956ae80693..38bd97186f2 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m @@ -38,7 +38,7 @@ - (void)apis { callable = [func HTTPSCallableWithName:@"name"]; FIRHTTPSCallableOptions *options = - [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; + [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm index ae1ad5a1524..7859755ac68 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm @@ -38,7 +38,7 @@ - (void)apis { callable = [func HTTPSCallableWithURL:url]; FIRHTTPSCallableOptions *options = - [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; + [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; From ad16f94e5e3959eeab5df4cac2ade55bc68d0d65 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 17:28:06 -0400 Subject: [PATCH 10/31] Fix some test build breakages --- FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift | 1 + FirebaseFunctions/Tests/Unit/FunctionsTests.swift | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift index 566823a4394..13e374aa327 100644 --- a/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift +++ b/FirebaseFunctions/Tests/CombineUnit/HTTPSCallableTests.swift @@ -33,6 +33,7 @@ class MockFunctions: Functions { var verifyParameters: ((_ name: String, _ data: Any?, _ timeout: TimeInterval) throws -> Void)? override func callFunction(name: String, withObject data: Any?, + options: HTTPSCallableOptions?, timeout: TimeInterval, completion: @escaping ((Result) -> Void)) { do { diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 3cccf0c21b6..02a6d48a905 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -127,7 +127,7 @@ class FunctionsTests: XCTestCase { } let completionExpectation = expectation(description: "completionExpectation") - functions?.callFunction(name: "fake_func", withObject: nil, timeout: 10) { result in + functions?.callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in switch result { case .success: XCTFail("Unexpected success from functions?.callFunction") @@ -156,7 +156,7 @@ class FunctionsTests: XCTestCase { } let completionExpectation = expectation(description: "completionExpectation") - functionsCustomDomain?.callFunction(name: "fake_func", withObject: nil, timeout: 10) { result in + functionsCustomDomain?.callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in switch result { case .success: XCTFail("Unexpected success from functions?.callFunction") From 02e8bdf36a1302251d4f777f202cfdd476498f9b Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 17:31:41 -0400 Subject: [PATCH 11/31] Add changelog entry --- FirebaseFunctions/CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FirebaseFunctions/CHANGELOG.md b/FirebaseFunctions/CHANGELOG.md index 0b5bebb6968..7e1c0fa465e 100644 --- a/FirebaseFunctions/CHANGELOG.md +++ b/FirebaseFunctions/CHANGELOG.md @@ -1,5 +1,7 @@ # 10.10.0 - [fixed] Fixed potential memory leak of Functions instances. (#11248) +- [added] Callable functions can now opt in to using limited use App Check + tokens. (#11270) # 10.0.0 - [fixed] Remove unnecessary and unused `encoder` and `decoder` parameters from From c813e82a601b173ddbe2354fb243c06191686402 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 17:33:55 -0400 Subject: [PATCH 12/31] Style --- .../Tests/Unit/FunctionsTests.swift | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 02a6d48a905..798572e1e45 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -127,15 +127,16 @@ class FunctionsTests: XCTestCase { } let completionExpectation = expectation(description: "completionExpectation") - functions?.callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in - switch result { - case .success: - XCTFail("Unexpected success from functions?.callFunction") - case let .failure(error as NSError): - XCTAssertEqual(error, networkError) + functions? + .callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in + switch result { + case .success: + XCTFail("Unexpected success from functions?.callFunction") + case let .failure(error as NSError): + XCTAssertEqual(error, networkError) + } + completionExpectation.fulfill() } - completionExpectation.fulfill() - } waitForExpectations(timeout: 1.5) } @@ -156,7 +157,12 @@ class FunctionsTests: XCTestCase { } let completionExpectation = expectation(description: "completionExpectation") - functionsCustomDomain?.callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in + functionsCustomDomain?.callFunction( + name: "fake_func", + withObject: nil, + options: nil, + timeout: 10 + ) { result in switch result { case .success: XCTFail("Unexpected success from functions?.callFunction") From 2cb3a62318dd8296ef7d0d939a43abd776bf9dff Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 17:37:11 -0400 Subject: [PATCH 13/31] Fix AppCheck tests --- SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m index d34d911a523..5e8374435bd 100644 --- a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m +++ b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m @@ -35,6 +35,10 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh }); } +- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler { + handler(self.tokenResult); +} + - (nonnull NSString *)notificationAppNameKey { return @"FakeAppCheckTokenDidChangeNotification"; } From a41565e5038b6c7a8b9c476a7729fd4eb13751dc Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 18:05:39 -0400 Subject: [PATCH 14/31] Cleanup current approach --- FirebaseFunctions/Sources/Functions.swift | 12 +++---- .../Sources/Internal/FunctionsContext.swift | 33 +++++++++---------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index aa6ba415926..691afc455c7 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -453,13 +453,11 @@ internal enum FunctionsConstants { forHTTPHeaderField: Constants.appCheckTokenHeader ) } - } else { - if let appCheckToken = context.appCheckToken { - fetcher.setRequestValue( - appCheckToken, - forHTTPHeaderField: Constants.appCheckTokenHeader - ) - } + } else if let appCheckToken = context.appCheckToken { + fetcher.setRequestValue( + appCheckToken, + forHTTPHeaderField: Constants.appCheckTokenHeader + ) } // Override normal security rules if this is a local test. diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 9852996f005..53568050899 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -51,7 +51,8 @@ internal class FunctionsContextProvider: NSObject { // // } - internal func getContext(_ completion: @escaping ((FunctionsContext, Error?) -> Void)) { + internal func getContext(options: HTTPSCallableOptions? = nil, + _ completion: @escaping ((FunctionsContext, Error?) -> Void)) { let dispatchGroup = DispatchGroup() var authToken: String? @@ -72,24 +73,22 @@ internal class FunctionsContextProvider: NSObject { if let appCheck = appCheck { dispatchGroup.enter() - appCheck.getToken(forcingRefresh: false) { tokenResult in - // Send only valid token to functions. - if tokenResult.error == nil { - appCheckToken = tokenResult.token + if options?.limitedUseAppCheckTokens == true { + appCheck.getLimitedUseToken? { tokenResult in + // Send only valid token to functions. + if tokenResult.error == nil { + limitedUseAppCheckToken = tokenResult.token + } + dispatchGroup.leave() } - dispatchGroup.leave() - } - } - - if let appCheck = appCheck { - dispatchGroup.enter() - - appCheck.getLimitedUseToken? { tokenResult in - // Send only valid token to functions. - if tokenResult.error == nil { - appCheckToken = tokenResult.token + } else { + appCheck.getToken(forcingRefresh: false) { tokenResult in + // Send only valid token to functions. + if tokenResult.error == nil { + appCheckToken = tokenResult.token + } + dispatchGroup.leave() } - dispatchGroup.leave() } } From e326a9721d698dd155a8256671b3996d4e347445 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 18:18:35 -0400 Subject: [PATCH 15/31] Re-organize tests --- .../Tests/Unit/FunctionsTests.swift | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 798572e1e45..ebec7fcef9d 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -106,7 +106,25 @@ class FunctionsTests: XCTestCase { XCTAssertEqual("http://localhost:1000", functions?.emulatorOrigin) } - // MARK: - App Check integration + /// Test that Functions instances get deallocated. + func testFunctionsLifecycle() throws { + weak var weakApp: FirebaseApp? + weak var weakFunctions: Functions? + try autoreleasepool { + let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", + gcmSenderID: "00000000000000000-00000000000-000000000") + options.projectID = "myProjectID" + let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) + weakApp = try XCTUnwrap(app1) + let functions = Functions(app: app1, region: "transitory-region", customDomain: nil) + weakFunctions = functions + XCTAssertNotNil(weakFunctions) + } + XCTAssertNil(weakApp) + XCTAssertNil(weakFunctions) + } + + // MARK: - App Check Integration func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() { appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) @@ -173,22 +191,4 @@ class FunctionsTests: XCTestCase { } waitForExpectations(timeout: 1.5) } - - /// Test that Functions instances get deallocated. - func testFunctionsLifecycle() throws { - weak var weakApp: FirebaseApp? - weak var weakFunctions: Functions? - try autoreleasepool { - let options = FirebaseOptions(googleAppID: "0:0000000000000:ios:0000000000000000", - gcmSenderID: "00000000000000000-00000000000-000000000") - options.projectID = "myProjectID" - let app1 = FirebaseApp(instanceWithName: "transitory app", options: options) - weakApp = try XCTUnwrap(app1) - let functions = Functions(app: app1, region: "transitory-region", customDomain: nil) - weakFunctions = functions - XCTAssertNotNil(weakFunctions) - } - XCTAssertNil(weakApp) - XCTAssertNil(weakFunctions) - } } From f5114ac6b3144b015d4b736bd085e7b301af3021 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Fri, 12 May 2023 18:55:06 -0400 Subject: [PATCH 16/31] Add 'options' param to 'getContext' call --- FirebaseFunctions/Sources/Functions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 691afc455c7..be05fc2220f 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -365,7 +365,7 @@ internal enum FunctionsConstants { timeout: TimeInterval, completion: @escaping ((Result) -> Void)) { // Get context first. - contextProvider.getContext { context, error in + contextProvider.getContext(options: options) { context, error in // Note: context is always non-nil since some checks could succeed, we're only failing if // there's an error. if let error = error { @@ -388,7 +388,7 @@ internal enum FunctionsConstants { timeout: TimeInterval, completion: @escaping ((Result) -> Void)) { // Get context first. - contextProvider.getContext { context, error in + contextProvider.getContext(options: options) { context, error in // Note: context is always non-nil since some checks could succeed, we're only failing if // there's an error. if let error = error { From 73d1e6c9bed7dd858aceb3782fe643fef4741dd4 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 13:45:26 -0400 Subject: [PATCH 17/31] Add success case unit test --- .../Tests/Unit/FunctionsTests.swift | 40 +++++++++++++++++++ .../AppCheckFake/FIRAppCheckFake.h | 3 ++ .../AppCheckFake/FIRAppCheckFake.m | 7 +++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index ebec7fcef9d..2cafadef31d 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -126,6 +126,46 @@ class FunctionsTests: XCTestCase { // MARK: - App Check Integration + func testCallFunctionWhenUsingLimitedUseAppCheckTokenThenTokenSuccess() { + // Given + // Stub returns of two different kinds of App Check tokens. Only the + // limited use token should be present in Functions's request header. + appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "shared_valid_token", error: nil) + appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( + token: "limited_use_valid_token", + error: nil + ) + + let httpRequestExpectation = expectation(description: "HTTPRequestExpectation") + fetcherService.testBlock = { fetcherToTest, testResponse in + let appCheckTokenHeader = fetcherToTest.request? + .value(forHTTPHeaderField: "X-Firebase-AppCheck") + // Assert that header contains limited use token. + XCTAssertEqual(appCheckTokenHeader, "limited_use_valid_token") + testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil) + httpRequestExpectation.fulfill() + } + + // When + let options = HTTPSCallableOptions(limitedUseAppCheckTokens: true) + + // Then + let completionExpectation = expectation(description: "completionExpectation") + functions? + .httpsCallable("fake_func", options: options) + .call { result, error in + guard let result = result else { + return XCTFail("Unexpected error: \(error!).") + } + + XCTAssertEqual(result.data as! String, "May the force be with you!") + + completionExpectation.fulfill() + } + + waitForExpectations(timeout: 1000.5) + } + func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() { appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) diff --git a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.h b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.h index 96101b19202..faeaf4cd1ec 100644 --- a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.h +++ b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.h @@ -25,6 +25,9 @@ NS_ASSUME_NONNULL_BEGIN /** The tokenResult to be passed to `-[getTokenWithCompletion:]` completion handler. */ @property(nonatomic, nonnull) id tokenResult; +/** The token result to be passed to `-[getLimitedUseTokenWithCompletion:]` completion handler. */ +@property(nonatomic, nonnull) id limitedUseTokenResult; + @end NS_ASSUME_NONNULL_END diff --git a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m index 5e8374435bd..4e669a94a23 100644 --- a/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m +++ b/SharedTestUtilities/AppCheckFake/FIRAppCheckFake.m @@ -24,6 +24,9 @@ - (instancetype)init { self = [super init]; if (self) { _tokenResult = [[FIRAppCheckTokenResultFake alloc] initWithToken:@"fake_valid_token" error:nil]; + _limitedUseTokenResult = + [[FIRAppCheckTokenResultFake alloc] initWithToken:@"fake_limited_use_valid_token" + error:nil]; } return self; } @@ -36,7 +39,9 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh } - (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler { - handler(self.tokenResult); + dispatch_async(dispatch_get_main_queue(), ^{ + handler(self.limitedUseTokenResult); + }); } - (nonnull NSString *)notificationAppNameKey { From 5d7f00a6c1a8627059045ae5eeb3a8645ed09755 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 14:17:52 -0400 Subject: [PATCH 18/31] Shorten expectation waiting timeout --- FirebaseFunctions/Tests/Unit/FunctionsTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 2cafadef31d..fef3bd703d8 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -163,7 +163,7 @@ class FunctionsTests: XCTestCase { completionExpectation.fulfill() } - waitForExpectations(timeout: 1000.5) + waitForExpectations(timeout: 1.5) } func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() { From c67a7300d8c28dd2f09dcb437acbd7fa39444165 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 14:37:41 -0400 Subject: [PATCH 19/31] Add remaining unit tests --- .../Tests/Unit/FunctionsTests.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index fef3bd703d8..378b4efe5d3 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -166,6 +166,82 @@ class FunctionsTests: XCTestCase { waitForExpectations(timeout: 1.5) } + func testCallFunctionWhenLimitedUseAppCheckTokenDisabledThenCallWithoutToken() { + // Given + appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( + token: "dummy 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") + } + + testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil) + httpRequestExpectation.fulfill() + } + + // When + let options = HTTPSCallableOptions(limitedUseAppCheckTokens: false) + + // Then + let completionExpectation = expectation(description: "completionExpectation") + functions? + .httpsCallable("fake_func", options: options) + .call { result, error in + guard let result = result else { + return XCTFail("Unexpected error: \(error!).") + } + + XCTAssertEqual(result.data as! String, "May the force be with you!") + + completionExpectation.fulfill() + } + + waitForExpectations(timeout: 1.5) + } + + func testCallFunctionWhenLimitedUseAppCheckTokenCannotBeGeneratedThenCallWithoutToken() { + // Given + appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( + token: "dummy 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") + } + + testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil) + httpRequestExpectation.fulfill() + } + + // When + let options = HTTPSCallableOptions(limitedUseAppCheckTokens: true) + + // Then + let completionExpectation = expectation(description: "completionExpectation") + functions? + .httpsCallable("fake_func", options: options) + .call { result, error in + guard let result = result else { + return XCTFail("Unexpected error: \(error!).") + } + + XCTAssertEqual(result.data as! String, "May the force be with you!") + + completionExpectation.fulfill() + } + + waitForExpectations(timeout: 1.5) + } + func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() { appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) From 6df26412c1dfda4394312807eca5b7ff69b83cb1 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 14:42:26 -0400 Subject: [PATCH 20/31] Amend current test --- .../Tests/Unit/FunctionsTests.swift | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 378b4efe5d3..6bd3fce43da 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -243,7 +243,13 @@ class FunctionsTests: XCTestCase { } func testCallFunctionWhenAppCheckIsInstalledAndFACTokenSuccess() { - appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "valid_token", error: nil) + // Stub returns of two different kinds of App Check tokens. Only the + // shared use token should be present in Functions's request header. + appCheckFake.tokenResult = FIRAppCheckTokenResultFake(token: "shared_valid_token", error: nil) + appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( + token: "limited_use_valid_token", + error: nil + ) let networkError = NSError( domain: "testCallFunctionWhenAppCheckIsInstalled", @@ -255,22 +261,24 @@ class FunctionsTests: XCTestCase { fetcherService.testBlock = { fetcherToTest, testResponse in let appCheckTokenHeader = fetcherToTest.request? .value(forHTTPHeaderField: "X-Firebase-AppCheck") - XCTAssertEqual(appCheckTokenHeader, "valid_token") + XCTAssertEqual(appCheckTokenHeader, "shared_valid_token") testResponse(nil, nil, networkError) httpRequestExpectation.fulfill() } let completionExpectation = expectation(description: "completionExpectation") functions? - .callFunction(name: "fake_func", withObject: nil, options: nil, timeout: 10) { result in - switch result { - case .success: - XCTFail("Unexpected success from functions?.callFunction") - case let .failure(error as NSError): - XCTAssertEqual(error, networkError) + .httpsCallable("fake_func") + .call { result, error in + guard let error = error else { + return XCTFail("Unexpected success: \(result!).") } + + XCTAssertEqual(error as NSError, networkError) + completionExpectation.fulfill() } + waitForExpectations(timeout: 1.5) } From 144068dfcdf925393ee63222e886c69610fa65d7 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 15:43:52 -0400 Subject: [PATCH 21/31] Rename 'limitedUseTokens' to 'requireLimitedUseTokens' --- FirebaseFunctions/Sources/Functions.swift | 2 +- FirebaseFunctions/Sources/HTTPSCallableOptions.swift | 4 ++-- FirebaseFunctions/Sources/Internal/FunctionsContext.swift | 2 +- FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index be05fc2220f..aec1f2ccad4 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -446,7 +446,7 @@ internal enum FunctionsConstants { fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader) } - if options?.limitedUseAppCheckTokens == true { + if options?.requireLimitedUseAppCheckTokens { if let appCheckToken = context.limitedUseAppCheckToken { fetcher.setRequestValue( appCheckToken, diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift index 26d8c4be258..de196137d21 100644 --- a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -17,12 +17,12 @@ import Foundation /// Configuration options for a ``HTTPSCallable`` instance. @objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject { /// Whether or not to protect the callable function with a limited use App Check token. - @objc public let limitedUseAppCheckTokens: Bool + @objc public let requireLimitedUseAppCheckTokens: Bool /// Designated intializer. /// - Parameter limitedUseAppCheckTokens: A boolean used to decide whether or not to /// protect the callable function with a limited use App Check token. @objc public init(limitedUseAppCheckTokens: Bool) { - self.limitedUseAppCheckTokens = limitedUseAppCheckTokens + requireLimitedUseAppCheckTokens = limitedUseAppCheckTokens } } diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 53568050899..2b110876ba1 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -73,7 +73,7 @@ internal class FunctionsContextProvider: NSObject { if let appCheck = appCheck { dispatchGroup.enter() - if options?.limitedUseAppCheckTokens == true { + if options?.requireLimitedUseAppCheckTokens { appCheck.getLimitedUseToken? { tokenResult in // Send only valid token to functions. if tokenResult.error == nil { diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m index 38bd97186f2..93adfe4bb6b 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m @@ -39,7 +39,7 @@ - (void)apis { FIRHTTPSCallableOptions *options = [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; - __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; + __unused BOOL limitedUseAppCheckTokens = options.requireLimitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; From 621c80acfd760b9f1e1a567b2d7ab21046351a87 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 16:12:14 -0400 Subject: [PATCH 22/31] Fix build and tests --- FirebaseFunctions/Sources/Functions.swift | 2 +- .../Sources/Internal/FunctionsContext.swift | 2 +- FirebaseFunctions/Tests/Unit/FunctionsTests.swift | 9 ++++++--- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index aec1f2ccad4..430d9d67009 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -446,7 +446,7 @@ internal enum FunctionsConstants { fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader) } - if options?.requireLimitedUseAppCheckTokens { + if options?.requireLimitedUseAppCheckTokens == true { if let appCheckToken = context.limitedUseAppCheckToken { fetcher.setRequestValue( appCheckToken, diff --git a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift index 2b110876ba1..b5a290e9f03 100644 --- a/FirebaseFunctions/Sources/Internal/FunctionsContext.swift +++ b/FirebaseFunctions/Sources/Internal/FunctionsContext.swift @@ -73,7 +73,7 @@ internal class FunctionsContextProvider: NSObject { if let appCheck = appCheck { dispatchGroup.enter() - if options?.requireLimitedUseAppCheckTokens { + if options?.requireLimitedUseAppCheckTokens == true { appCheck.getLimitedUseToken? { tokenResult in // Send only valid token to functions. if tokenResult.error == nil { diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 6bd3fce43da..52a02ccd21a 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -168,16 +168,19 @@ class FunctionsTests: XCTestCase { func testCallFunctionWhenLimitedUseAppCheckTokenDisabledThenCallWithoutToken() { // Given + let limitedUseDummyToken = "limited use dummy token" appCheckFake.limitedUseTokenResult = FIRAppCheckTokenResultFake( - token: "dummy token", + token: limitedUseDummyToken, 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") + fetcherToTest.request?.allHTTPHeaderFields?.forEach { key, value in + if key == "X-Firebase-AppCheck" { + XCTAssertNotEqual(value, limitedUseDummyToken) + } } testResponse(nil, "{\"data\":\"May the force be with you!\"}".data(using: .utf8), nil) From 8b9d19ad2e3acc3c7cdb9335b1f0d519b996abd0 Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 16:30:45 -0400 Subject: [PATCH 23/31] Fix some renaming misses --- FirebaseFunctions/Sources/HTTPSCallableOptions.swift | 6 +++--- FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m | 4 ++-- FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm | 2 +- FirebaseFunctions/Tests/Unit/FunctionsTests.swift | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift index de196137d21..223a309b3d9 100644 --- a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -20,9 +20,9 @@ import Foundation @objc public let requireLimitedUseAppCheckTokens: Bool /// Designated intializer. - /// - Parameter limitedUseAppCheckTokens: A boolean used to decide whether or not to + /// - Parameter requireLimitedUseAppCheckTokens: A boolean used to decide whether or not to /// protect the callable function with a limited use App Check token. - @objc public init(limitedUseAppCheckTokens: Bool) { - requireLimitedUseAppCheckTokens = limitedUseAppCheckTokens + @objc public init(requireLimitedUseAppCheckTokens: Bool) { + self.requireLimitedUseAppCheckTokens = requireLimitedUseAppCheckTokens } } diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m index 93adfe4bb6b..f6d0fd6e13b 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCAPITests.m @@ -38,8 +38,8 @@ - (void)apis { callable = [func HTTPSCallableWithName:@"name"]; FIRHTTPSCallableOptions *options = - [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; - __unused BOOL limitedUseAppCheckTokens = options.requireLimitedUseAppCheckTokens; + [[FIRHTTPSCallableOptions alloc] initWithRequireLimitedUseAppCheckTokens:YES]; + __unused BOOL requireLimitedUseAppCheckTokens = options.requireLimitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm index 7859755ac68..8c96c78098d 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm @@ -39,7 +39,7 @@ - (void)apis { FIRHTTPSCallableOptions *options = [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; - __unused BOOL limitedUseAppCheckTokens = options.limitedUseAppCheckTokens; + __unused BOOL requireLimitedUseAppCheckTokens = options.requireLimitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; diff --git a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift index 52a02ccd21a..1ac26db868d 100644 --- a/FirebaseFunctions/Tests/Unit/FunctionsTests.swift +++ b/FirebaseFunctions/Tests/Unit/FunctionsTests.swift @@ -147,7 +147,7 @@ class FunctionsTests: XCTestCase { } // When - let options = HTTPSCallableOptions(limitedUseAppCheckTokens: true) + let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: true) // Then let completionExpectation = expectation(description: "completionExpectation") @@ -188,7 +188,7 @@ class FunctionsTests: XCTestCase { } // When - let options = HTTPSCallableOptions(limitedUseAppCheckTokens: false) + let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: false) // Then let completionExpectation = expectation(description: "completionExpectation") @@ -226,7 +226,7 @@ class FunctionsTests: XCTestCase { } // When - let options = HTTPSCallableOptions(limitedUseAppCheckTokens: true) + let options = HTTPSCallableOptions(requireLimitedUseAppCheckTokens: true) // Then let completionExpectation = expectation(description: "completionExpectation") From 2e038c14dfcdca79aa6ca730734d93b46ef609be Mon Sep 17 00:00:00 2001 From: Nick Cooke Date: Mon, 15 May 2023 16:33:07 -0400 Subject: [PATCH 24/31] Fix some renaming misses (1) --- FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm index 8c96c78098d..33979641ab2 100644 --- a/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm +++ b/FirebaseFunctions/Tests/ObjCIntegration/ObjCPPAPITests.mm @@ -38,7 +38,7 @@ - (void)apis { callable = [func HTTPSCallableWithURL:url]; FIRHTTPSCallableOptions *options = - [[FIRHTTPSCallableOptions alloc] initWithLimitedUseAppCheckTokens:YES]; + [[FIRHTTPSCallableOptions alloc] initWithRequireLimitedUseAppCheckTokens:YES]; __unused BOOL requireLimitedUseAppCheckTokens = options.requireLimitedUseAppCheckTokens; callable = [func HTTPSCallableWithURL:url options:options]; callable = [func HTTPSCallableWithName:@"name" options:options]; From 09183c147ec53c48666c760eefe0f7019885fb4a Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:14 -0400 Subject: [PATCH 25/31] Update FirebaseFunctions/CHANGELOG.md Co-authored-by: Andrew Heard --- FirebaseFunctions/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/CHANGELOG.md b/FirebaseFunctions/CHANGELOG.md index 7e1c0fa465e..c156c6daffd 100644 --- a/FirebaseFunctions/CHANGELOG.md +++ b/FirebaseFunctions/CHANGELOG.md @@ -1,6 +1,6 @@ # 10.10.0 - [fixed] Fixed potential memory leak of Functions instances. (#11248) -- [added] Callable functions can now opt in to using limited use App Check +- [added] Callable functions can now opt in to using limited-use App Check tokens. (#11270) # 10.0.0 From e304a1aaf8d0d9081c297f0dd0a72c4fc22861de Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:31 -0400 Subject: [PATCH 26/31] Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard --- FirebaseFunctions/Sources/Functions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 430d9d67009..aae4df7bf8f 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -177,8 +177,8 @@ internal enum FunctionsConstants { /// - name: The name of the Callable HTTPS trigger /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - encoder: The encoder instance to use to run the encoding. - /// - decoder: The decoder instance to use to run the decoding. + /// - encoder: The encoder instance to use to perform encoding. + /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ name: String, From 7391807f8706ccca26a8aa5e04bf47ee98260273 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:37 -0400 Subject: [PATCH 27/31] Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard --- FirebaseFunctions/Sources/Functions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index aae4df7bf8f..4ce920799ff 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -203,8 +203,8 @@ internal enum FunctionsConstants { /// - options: The options with which to customize the Callable HTTPS trigger. /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - encoder: The encoder instance to use to run the encoding. - /// - decoder: The decoder instance to use to run the decoding. + /// - encoder: The encoder instance to use to perform encoding. + /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ name: String, From 6b919239b35a46369c997852c0a782c3d33e19d4 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:43 -0400 Subject: [PATCH 28/31] Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard --- FirebaseFunctions/Sources/Functions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 4ce920799ff..2469f41cb0c 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -229,8 +229,8 @@ internal enum FunctionsConstants { /// - url: The url of the Callable HTTPS trigger /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - encoder: The encoder instance to use to run the encoding. - /// - decoder: The decoder instance to use to run the decoding. + /// - encoder: The encoder instance to use to perform encoding. + /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ url: URL, From d5ef755ea11d9dc30f4af9ffc964b80cc030e130 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:48 -0400 Subject: [PATCH 29/31] Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard --- FirebaseFunctions/Sources/Functions.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FirebaseFunctions/Sources/Functions.swift b/FirebaseFunctions/Sources/Functions.swift index 2469f41cb0c..6ab05324cb3 100644 --- a/FirebaseFunctions/Sources/Functions.swift +++ b/FirebaseFunctions/Sources/Functions.swift @@ -255,8 +255,8 @@ internal enum FunctionsConstants { /// - options: The options with which to customize the Callable HTTPS trigger. /// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable` /// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable` - /// - encoder: The encoder instance to use to run the encoding. - /// - decoder: The decoder instance to use to run the decoding. + /// - encoder: The encoder instance to use to perform encoding. + /// - decoder: The decoder instance to use to perform decoding. /// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations. open func httpsCallable(_ url: URL, From 16baf4460ed2ac38457b967d0b93f2b525a237cd Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Mon, 15 May 2023 17:39:56 -0400 Subject: [PATCH 30/31] Update FirebaseFunctions/Sources/HTTPSCallableOptions.swift Co-authored-by: Andrew Heard --- FirebaseFunctions/Sources/HTTPSCallableOptions.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift index 223a309b3d9..bdd45ac8bbf 100644 --- a/FirebaseFunctions/Sources/HTTPSCallableOptions.swift +++ b/FirebaseFunctions/Sources/HTTPSCallableOptions.swift @@ -16,7 +16,7 @@ import Foundation /// Configuration options for a ``HTTPSCallable`` instance. @objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject { - /// Whether or not to protect the callable function with a limited use App Check token. + /// Whether or not to protect the callable function with a limited-use App Check token. @objc public let requireLimitedUseAppCheckTokens: Bool /// Designated intializer. From e30f75db903f77896d3a4669604fa6f51f5dcd48 Mon Sep 17 00:00:00 2001 From: Andrew Heard Date: Tue, 16 May 2023 17:58:07 -0400 Subject: [PATCH 31/31] [App Check] Always return fresh limitedUseToken (#11298) Updated `limitedUseTokenWithCompletion:` to always return a fresh limited-used token in every invocation (removed the `ongoingLimitedUseTokenPromise` property). --- FirebaseAppCheck/Sources/Core/FIRAppCheck.m | 25 ++------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m index 02b10bb1be3..9aebc19b5c5 100644 --- a/FirebaseAppCheck/Sources/Core/FIRAppCheck.m +++ b/FirebaseAppCheck/Sources/Core/FIRAppCheck.m @@ -72,7 +72,6 @@ @interface FIRAppCheck () @property(nonatomic, readonly, nullable) id tokenRefresher; @property(nonatomic, nullable) FBLPromise *ongoingRetrieveOrRefreshTokenPromise; -@property(nonatomic, nullable) FBLPromise *ongoingLimitedUseTokenPromise; @end @implementation FIRAppCheck @@ -180,7 +179,7 @@ - (void)tokenForcingRefresh:(BOOL)forcingRefresh - (void)limitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token, NSError *_Nullable error))handler { - [self retrieveLimitedUseToken] + [self limitedUseToken] .then(^id _Nullable(FIRAppCheckToken *token) { handler(token, nil); return token; @@ -235,7 +234,7 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh } - (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler { - [self retrieveLimitedUseToken] + [self limitedUseToken] .then(^id _Nullable(FIRAppCheckToken *token) { FIRAppCheckTokenResult *result = [[FIRAppCheckTokenResult alloc] initWithToken:token.token error:nil]; @@ -314,26 +313,6 @@ - (nonnull NSString *)notificationTokenKey { }); } -- (FBLPromise *)retrieveLimitedUseToken { - return [FBLPromise do:^id _Nullable { - if (self.ongoingLimitedUseTokenPromise == nil) { - // Kick off a new operation only when there is not an ongoing one. - self.ongoingLimitedUseTokenPromise = - [self limitedUseToken] - // Release the ongoing operation promise on completion. - .then(^FIRAppCheckToken *(FIRAppCheckToken *token) { - self.ongoingLimitedUseTokenPromise = nil; - return token; - }) - .recover(^NSError *(NSError *error) { - self.ongoingLimitedUseTokenPromise = nil; - return error; - }); - } - return self.ongoingLimitedUseTokenPromise; - }]; -} - - (FBLPromise *)refreshToken { return [FBLPromise wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {