Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
bb8df26
draft pr for callable functions
aiwenisevan May 10, 2023
1e9b989
Adjust Functions podspec dependency on AppCheckInterop
ncooke3 May 12, 2023
3fc0cce
Add 'HTTPSCallableOptions' class
ncooke3 May 12, 2023
aa7d457
Re-implement with new HTTPSCallableOptions type
ncooke3 May 12, 2023
893f74e
Revert and update Objective-C API tests
ncooke3 May 12, 2023
765534d
Mark new AppCheckInterop API optional for backwards compatibility
ncooke3 May 12, 2023
ad5d68b
Preserve existing codable Functions API
ncooke3 May 12, 2023
67a49df
Revert integration tests
ncooke3 May 12, 2023
846eb22
Style
ncooke3 May 12, 2023
ad16f94
Fix some test build breakages
ncooke3 May 12, 2023
02e8bdf
Add changelog entry
ncooke3 May 12, 2023
c813e82
Style
ncooke3 May 12, 2023
2cb3a62
Fix AppCheck tests
ncooke3 May 12, 2023
a41565e
Cleanup current approach
ncooke3 May 12, 2023
e326a97
Re-organize tests
ncooke3 May 12, 2023
f5114ac
Add 'options' param to 'getContext' call
ncooke3 May 12, 2023
73d1e6c
Add success case unit test
ncooke3 May 15, 2023
5d7f00a
Shorten expectation waiting timeout
ncooke3 May 15, 2023
c67a730
Add remaining unit tests
ncooke3 May 15, 2023
6df2641
Amend current test
ncooke3 May 15, 2023
144068d
Rename 'limitedUseTokens' to 'requireLimitedUseTokens'
ncooke3 May 15, 2023
621c80a
Fix build and tests
ncooke3 May 15, 2023
8b9d19a
Fix some renaming misses
ncooke3 May 15, 2023
2e038c1
Fix some renaming misses (1)
ncooke3 May 15, 2023
09183c1
Update FirebaseFunctions/CHANGELOG.md
ncooke3 May 15, 2023
e304a1a
Update FirebaseFunctions/Sources/Functions.swift
ncooke3 May 15, 2023
7391807
Update FirebaseFunctions/Sources/Functions.swift
ncooke3 May 15, 2023
6b91923
Update FirebaseFunctions/Sources/Functions.swift
ncooke3 May 15, 2023
d5ef755
Update FirebaseFunctions/Sources/Functions.swift
ncooke3 May 15, 2023
16baf44
Update FirebaseFunctions/Sources/HTTPSCallableOptions.swift
ncooke3 May 15, 2023
e30f75d
[App Check] Always return fresh limitedUseToken (#11298)
andrewheard May 16, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions FirebaseAppCheck/Interop/FIRAppCheckInterop.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,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
38 changes: 16 additions & 22 deletions FirebaseAppCheck/Sources/Core/FIRAppCheck.m
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@ @interface FIRAppCheck () <FIRAppCheckInterop>
@property(nonatomic, readonly, nullable) id<FIRAppCheckTokenRefresherProtocol> tokenRefresher;

@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;
@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingLimitedUseTokenPromise;
@end

@implementation FIRAppCheck
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -234,6 +233,21 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh
});
}

- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler {
[self limitedUseToken]
.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;
}
Expand Down Expand Up @@ -299,26 +313,6 @@ - (nonnull NSString *)notificationTokenKey {
});
}

- (FBLPromise<FIRAppCheckToken *> *)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<FIRAppCheckToken *> *)refreshToken {
return [FBLPromise
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {
Expand Down
2 changes: 1 addition & 1 deletion FirebaseFunctions.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
2 changes: 2 additions & 0 deletions FirebaseFunctions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
143 changes: 123 additions & 20 deletions FirebaseFunctions/Sources/Functions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -135,25 +135,50 @@ 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.
*/
/// 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)
}

/// 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
/// - 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 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<Request: Encodable,
Response: Decodable>(_ name: String,
Expand All @@ -164,16 +189,48 @@ internal enum FunctionsConstants {
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
))
-> Callable<Request, Response> {
return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder)
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.
/// - 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:
/// - 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 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<Request: Encodable,
Response: Decodable>(_ name: String,
options: HTTPSCallableOptions,
requestAs: Request.Type = Request.self,
responseAs: Response.Type = Response.self,
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
),
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
))
-> Callable<Request, Response> {
return Callable(
callable: httpsCallable(name, options: options),
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:
/// - 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 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<Request: Encodable,
Response: Decodable>(_ url: URL,
Expand All @@ -184,7 +241,38 @@ internal enum FunctionsConstants {
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
))
-> Callable<Request, Response> {
return Callable(callable: httpsCallable(url), encoder: encoder, decoder: decoder)
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:
/// - 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 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<Request: Encodable,
Response: Decodable>(_ url: URL,
options: HTTPSCallableOptions,
requestAs: Request.Type = Request.self,
responseAs: Response.Type = Response.self,
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
),
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
))
-> Callable<Request, Response> {
return Callable(
callable: httpsCallable(url, options: options),
encoder: encoder,
decoder: decoder
)
}

/**
Expand Down Expand Up @@ -273,10 +361,11 @@ internal enum FunctionsConstants {

internal func callFunction(name: String,
withObject data: Any?,
options: HTTPSCallableOptions?,
timeout: TimeInterval,
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> 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 {
Expand All @@ -285,6 +374,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)
Expand All @@ -294,17 +384,19 @@ internal enum FunctionsConstants {

internal func callFunction(url: URL,
withObject data: Any?,
options: HTTPSCallableOptions?,
timeout: TimeInterval,
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> 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 {
completion(.failure(error))
} else {
self.callFunction(url: url,
withObject: data,
options: options,
timeout: timeout,
context: context,
completion: completion)
Expand All @@ -314,6 +406,7 @@ internal enum FunctionsConstants {

private func callFunction(url: URL,
withObject data: Any?,
options: HTTPSCallableOptions?,
timeout: TimeInterval,
context: FunctionsContext,
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
Expand Down Expand Up @@ -353,8 +446,18 @@ internal enum FunctionsConstants {
fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader)
}

if let appCheckToken = context.appCheckToken {
fetcher.setRequestValue(appCheckToken, forHTTPHeaderField: Constants.appCheckTokenHeader)
if options?.requireLimitedUseAppCheckTokens == 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.
Expand Down
10 changes: 8 additions & 2 deletions FirebaseFunctions/Sources/HTTPSCallable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,20 +50,24 @@ open class HTTPSCallable: NSObject {

private let endpoint: EndpointType

private let options: HTTPSCallableOptions?

// MARK: - Public Properties

/**
* The timeout to use when calling the function. Defaults to 70 seconds.
*/
@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)
}

Expand Down Expand Up @@ -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)
}
Expand Down
28 changes: 28 additions & 0 deletions FirebaseFunctions/Sources/HTTPSCallableOptions.swift
Original file line number Diff line number Diff line change
@@ -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 requireLimitedUseAppCheckTokens: Bool

/// Designated intializer.
/// - 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(requireLimitedUseAppCheckTokens: Bool) {
self.requireLimitedUseAppCheckTokens = requireLimitedUseAppCheckTokens
}
}
Loading