Skip to content

Commit 0cbe4f2

Browse files
aiwenisevanncooke3andrewheard
authored
Firebase App Check One-time Use Tokens for Callable Functions SDK (#11270)
* draft pr for callable functions * Adjust Functions podspec dependency on AppCheckInterop * Add 'HTTPSCallableOptions' class * Re-implement with new HTTPSCallableOptions type * Revert and update Objective-C API tests * Mark new AppCheckInterop API optional for backwards compatibility * Preserve existing codable Functions API * Revert integration tests * Style * Fix some test build breakages * Add changelog entry * Style * Fix AppCheck tests * Cleanup current approach * Re-organize tests * Add 'options' param to 'getContext' call * Add success case unit test * Shorten expectation waiting timeout * Add remaining unit tests * Amend current test * Rename 'limitedUseTokens' to 'requireLimitedUseTokens' * Fix build and tests * Fix some renaming misses * Fix some renaming misses (1) * Update FirebaseFunctions/CHANGELOG.md Co-authored-by: Andrew Heard <andrewheard@google.com> * Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard <andrewheard@google.com> * Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard <andrewheard@google.com> * Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard <andrewheard@google.com> * Update FirebaseFunctions/Sources/Functions.swift Co-authored-by: Andrew Heard <andrewheard@google.com> * Update FirebaseFunctions/Sources/HTTPSCallableOptions.swift Co-authored-by: Andrew Heard <andrewheard@google.com> * [App Check] Always return fresh limitedUseToken (#11298) Updated `limitedUseTokenWithCompletion:` to always return a fresh limited-used token in every invocation (removed the `ongoingLimitedUseTokenPromise` property). --------- Co-authored-by: Nick Cooke <nickcooke@google.com> Co-authored-by: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Co-authored-by: Andrew Heard <andrewheard@google.com>
1 parent 7534914 commit 0cbe4f2

File tree

14 files changed

+398
-83
lines changed

14 files changed

+398
-83
lines changed

FirebaseAppCheck/Interop/FIRAppCheckInterop.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,14 @@ NS_SWIFT_NAME(AppCheckInterop) @protocol FIRAppCheckInterop
4343
/// `tokenDidChangeNotificationName`.
4444
- (NSString *)notificationAppNameKey;
4545

46+
// MARK: - Optional API
47+
48+
@optional
49+
50+
/// Retrieve a new limited-use Firebase App Check token
51+
- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler
52+
NS_SWIFT_NAME(getLimitedUseToken(completion:));
53+
4654
@end
4755

4856
NS_ASSUME_NONNULL_END

FirebaseAppCheck/Sources/Core/FIRAppCheck.m

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,6 @@ @interface FIRAppCheck () <FIRAppCheckInterop>
7272
@property(nonatomic, readonly, nullable) id<FIRAppCheckTokenRefresherProtocol> tokenRefresher;
7373

7474
@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingRetrieveOrRefreshTokenPromise;
75-
@property(nonatomic, nullable) FBLPromise<FIRAppCheckToken *> *ongoingLimitedUseTokenPromise;
7675
@end
7776

7877
@implementation FIRAppCheck
@@ -180,7 +179,7 @@ - (void)tokenForcingRefresh:(BOOL)forcingRefresh
180179

181180
- (void)limitedUseTokenWithCompletion:(void (^)(FIRAppCheckToken *_Nullable token,
182181
NSError *_Nullable error))handler {
183-
[self retrieveLimitedUseToken]
182+
[self limitedUseToken]
184183
.then(^id _Nullable(FIRAppCheckToken *token) {
185184
handler(token, nil);
186185
return token;
@@ -234,6 +233,21 @@ - (void)getTokenForcingRefresh:(BOOL)forcingRefresh
234233
});
235234
}
236235

236+
- (void)getLimitedUseTokenWithCompletion:(FIRAppCheckTokenHandlerInterop)handler {
237+
[self limitedUseToken]
238+
.then(^id _Nullable(FIRAppCheckToken *token) {
239+
FIRAppCheckTokenResult *result = [[FIRAppCheckTokenResult alloc] initWithToken:token.token
240+
error:nil];
241+
handler(result);
242+
return result;
243+
})
244+
.catch(^(NSError *_Nonnull error) {
245+
FIRAppCheckTokenResult *result =
246+
[[FIRAppCheckTokenResult alloc] initWithToken:kDummyFACTokenValue error:error];
247+
handler(result);
248+
});
249+
}
250+
237251
- (nonnull NSString *)tokenDidChangeNotificationName {
238252
return FIRAppCheckAppCheckTokenDidChangeNotification;
239253
}
@@ -299,26 +313,6 @@ - (nonnull NSString *)notificationTokenKey {
299313
});
300314
}
301315

302-
- (FBLPromise<FIRAppCheckToken *> *)retrieveLimitedUseToken {
303-
return [FBLPromise do:^id _Nullable {
304-
if (self.ongoingLimitedUseTokenPromise == nil) {
305-
// Kick off a new operation only when there is not an ongoing one.
306-
self.ongoingLimitedUseTokenPromise =
307-
[self limitedUseToken]
308-
// Release the ongoing operation promise on completion.
309-
.then(^FIRAppCheckToken *(FIRAppCheckToken *token) {
310-
self.ongoingLimitedUseTokenPromise = nil;
311-
return token;
312-
})
313-
.recover(^NSError *(NSError *error) {
314-
self.ongoingLimitedUseTokenPromise = nil;
315-
return error;
316-
});
317-
}
318-
return self.ongoingLimitedUseTokenPromise;
319-
}];
320-
}
321-
322316
- (FBLPromise<FIRAppCheckToken *> *)refreshToken {
323317
return [FBLPromise
324318
wrapObjectOrErrorCompletion:^(FBLPromiseObjectOrErrorCompletion _Nonnull handler) {

FirebaseFunctions.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ Cloud Functions for Firebase.
3939

4040
s.dependency 'FirebaseCore', '~> 10.0'
4141
s.dependency 'FirebaseCoreExtension', '~> 10.0'
42-
s.dependency 'FirebaseAppCheckInterop', '~> 10.0'
42+
s.dependency 'FirebaseAppCheckInterop', '~> 10.10'
4343
s.dependency 'FirebaseAuthInterop', '~> 10.0'
4444
s.dependency 'FirebaseMessagingInterop', '~> 10.0'
4545
s.dependency 'FirebaseSharedSwift', '~> 10.0'

FirebaseFunctions/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# 10.10.0
22
- [fixed] Fixed potential memory leak of Functions instances. (#11248)
3+
- [added] Callable functions can now opt in to using limited-use App Check
4+
tokens. (#11270)
35

46
# 10.0.0
57
- [fixed] Remove unnecessary and unused `encoder` and `decoder` parameters from

FirebaseFunctions/Sources/Functions.swift

Lines changed: 123 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -135,25 +135,50 @@ internal enum FunctionsConstants {
135135
return functions(app: app, region: FunctionsConstants.defaultRegion, customDomain: customDomain)
136136
}
137137

138-
/**
139-
* Creates a reference to the Callable HTTPS trigger with the given name.
140-
* - Parameter name The name of the Callable HTTPS trigger.
141-
*/
138+
/// Creates a reference to the Callable HTTPS trigger with the given name.
139+
/// - Parameter name: The name of the Callable HTTPS trigger.
140+
/// - Returns: A reference to a Callable HTTPS trigger.
142141
@objc(HTTPSCallableWithName:) open func httpsCallable(_ name: String) -> HTTPSCallable {
143142
return HTTPSCallable(functions: self, name: name)
144143
}
145144

145+
/// Creates a reference to the Callable HTTPS trigger with the given name and configuration options.
146+
/// - Parameters:
147+
/// - name: The name of the Callable HTTPS trigger.
148+
/// - options: The options with which to customize the Callable HTTPS trigger.
149+
/// - Returns: A reference to a Callable HTTPS trigger.
150+
@objc(HTTPSCallableWithName:options:) public func httpsCallable(_ name: String,
151+
options: HTTPSCallableOptions)
152+
-> HTTPSCallable {
153+
return HTTPSCallable(functions: self, name: name, options: options)
154+
}
155+
156+
/// Creates a reference to the Callable HTTPS trigger with the given name.
157+
/// - Parameter url: The URL of the Callable HTTPS trigger.
158+
/// - Returns: A reference to a Callable HTTPS trigger.
146159
@objc(HTTPSCallableWithURL:) open func httpsCallable(_ url: URL) -> HTTPSCallable {
147160
return HTTPSCallable(functions: self, url: url)
148161
}
149162

163+
/// Creates a reference to the Callable HTTPS trigger with the given name and configuration options.
164+
/// - Parameters:
165+
/// - url: The URL of the Callable HTTPS trigger.
166+
/// - options: The options with which to customize the Callable HTTPS trigger.
167+
/// - Returns: A reference to a Callable HTTPS trigger.
168+
@objc(HTTPSCallableWithURL:options:) public func httpsCallable(_ url: URL,
169+
options: HTTPSCallableOptions)
170+
-> HTTPSCallable {
171+
return HTTPSCallable(functions: self, url: url, options: options)
172+
}
173+
150174
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
151175
/// request and the type of a `Decodable` response.
152-
/// - Parameter name: The name of the Callable HTTPS trigger
153-
/// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
154-
/// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
155-
/// - Parameter encoder: The encoder instance to use to run the encoding.
156-
/// - Parameter decoder: The decoder instance to use to run the decoding.
176+
/// - Parameters:
177+
/// - name: The name of the Callable HTTPS trigger
178+
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
179+
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
180+
/// - encoder: The encoder instance to use to perform encoding.
181+
/// - decoder: The decoder instance to use to perform decoding.
157182
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations.
158183
open func httpsCallable<Request: Encodable,
159184
Response: Decodable>(_ name: String,
@@ -164,16 +189,48 @@ internal enum FunctionsConstants {
164189
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
165190
))
166191
-> Callable<Request, Response> {
167-
return Callable(callable: httpsCallable(name), encoder: encoder, decoder: decoder)
192+
return Callable(
193+
callable: httpsCallable(name),
194+
encoder: encoder,
195+
decoder: decoder
196+
)
168197
}
169198

170199
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
171200
/// request and the type of a `Decodable` response.
172-
/// - Parameter url: The url of the Callable HTTPS trigger
173-
/// - Parameter requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
174-
/// - Parameter responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
175-
/// - Parameter encoder: The encoder instance to use to run the encoding.
176-
/// - Parameter decoder: The decoder instance to use to run the decoding.
201+
/// - Parameters:
202+
/// - name: The name of the Callable HTTPS trigger
203+
/// - options: The options with which to customize the Callable HTTPS trigger.
204+
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
205+
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
206+
/// - encoder: The encoder instance to use to perform encoding.
207+
/// - decoder: The decoder instance to use to perform decoding.
208+
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations.
209+
open func httpsCallable<Request: Encodable,
210+
Response: Decodable>(_ name: String,
211+
options: HTTPSCallableOptions,
212+
requestAs: Request.Type = Request.self,
213+
responseAs: Response.Type = Response.self,
214+
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
215+
),
216+
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
217+
))
218+
-> Callable<Request, Response> {
219+
return Callable(
220+
callable: httpsCallable(name, options: options),
221+
encoder: encoder,
222+
decoder: decoder
223+
)
224+
}
225+
226+
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
227+
/// request and the type of a `Decodable` response.
228+
/// - Parameters:
229+
/// - url: The url of the Callable HTTPS trigger
230+
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
231+
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
232+
/// - encoder: The encoder instance to use to perform encoding.
233+
/// - decoder: The decoder instance to use to perform decoding.
177234
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations.
178235
open func httpsCallable<Request: Encodable,
179236
Response: Decodable>(_ url: URL,
@@ -184,7 +241,38 @@ internal enum FunctionsConstants {
184241
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
185242
))
186243
-> Callable<Request, Response> {
187-
return Callable(callable: httpsCallable(url), encoder: encoder, decoder: decoder)
244+
return Callable(
245+
callable: httpsCallable(url),
246+
encoder: encoder,
247+
decoder: decoder
248+
)
249+
}
250+
251+
/// Creates a reference to the Callable HTTPS trigger with the given name, the type of an `Encodable`
252+
/// request and the type of a `Decodable` response.
253+
/// - Parameters:
254+
/// - url: The url of the Callable HTTPS trigger
255+
/// - options: The options with which to customize the Callable HTTPS trigger.
256+
/// - requestAs: The type of the `Encodable` entity to use for requests to this `Callable`
257+
/// - responseAs: The type of the `Decodable` entity to use for responses from this `Callable`
258+
/// - encoder: The encoder instance to use to perform encoding.
259+
/// - decoder: The decoder instance to use to perform decoding.
260+
/// - Returns: A reference to an HTTPS-callable Cloud Function that can be used to make Cloud Functions invocations.
261+
open func httpsCallable<Request: Encodable,
262+
Response: Decodable>(_ url: URL,
263+
options: HTTPSCallableOptions,
264+
requestAs: Request.Type = Request.self,
265+
responseAs: Response.Type = Response.self,
266+
encoder: FirebaseDataEncoder = FirebaseDataEncoder(
267+
),
268+
decoder: FirebaseDataDecoder = FirebaseDataDecoder(
269+
))
270+
-> Callable<Request, Response> {
271+
return Callable(
272+
callable: httpsCallable(url, options: options),
273+
encoder: encoder,
274+
decoder: decoder
275+
)
188276
}
189277

190278
/**
@@ -273,10 +361,11 @@ internal enum FunctionsConstants {
273361

274362
internal func callFunction(name: String,
275363
withObject data: Any?,
364+
options: HTTPSCallableOptions?,
276365
timeout: TimeInterval,
277366
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
278367
// Get context first.
279-
contextProvider.getContext { context, error in
368+
contextProvider.getContext(options: options) { context, error in
280369
// Note: context is always non-nil since some checks could succeed, we're only failing if
281370
// there's an error.
282371
if let error = error {
@@ -285,6 +374,7 @@ internal enum FunctionsConstants {
285374
let url = self.urlWithName(name)
286375
self.callFunction(url: URL(string: url)!,
287376
withObject: data,
377+
options: options,
288378
timeout: timeout,
289379
context: context,
290380
completion: completion)
@@ -294,17 +384,19 @@ internal enum FunctionsConstants {
294384

295385
internal func callFunction(url: URL,
296386
withObject data: Any?,
387+
options: HTTPSCallableOptions?,
297388
timeout: TimeInterval,
298389
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
299390
// Get context first.
300-
contextProvider.getContext { context, error in
391+
contextProvider.getContext(options: options) { context, error in
301392
// Note: context is always non-nil since some checks could succeed, we're only failing if
302393
// there's an error.
303394
if let error = error {
304395
completion(.failure(error))
305396
} else {
306397
self.callFunction(url: url,
307398
withObject: data,
399+
options: options,
308400
timeout: timeout,
309401
context: context,
310402
completion: completion)
@@ -314,6 +406,7 @@ internal enum FunctionsConstants {
314406

315407
private func callFunction(url: URL,
316408
withObject data: Any?,
409+
options: HTTPSCallableOptions?,
317410
timeout: TimeInterval,
318411
context: FunctionsContext,
319412
completion: @escaping ((Result<HTTPSCallableResult, Error>) -> Void)) {
@@ -353,8 +446,18 @@ internal enum FunctionsConstants {
353446
fetcher.setRequestValue(fcmToken, forHTTPHeaderField: Constants.fcmTokenHeader)
354447
}
355448

356-
if let appCheckToken = context.appCheckToken {
357-
fetcher.setRequestValue(appCheckToken, forHTTPHeaderField: Constants.appCheckTokenHeader)
449+
if options?.requireLimitedUseAppCheckTokens == true {
450+
if let appCheckToken = context.limitedUseAppCheckToken {
451+
fetcher.setRequestValue(
452+
appCheckToken,
453+
forHTTPHeaderField: Constants.appCheckTokenHeader
454+
)
455+
}
456+
} else if let appCheckToken = context.appCheckToken {
457+
fetcher.setRequestValue(
458+
appCheckToken,
459+
forHTTPHeaderField: Constants.appCheckTokenHeader
460+
)
358461
}
359462

360463
// Override normal security rules if this is a local test.

FirebaseFunctions/Sources/HTTPSCallable.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,20 +50,24 @@ open class HTTPSCallable: NSObject {
5050

5151
private let endpoint: EndpointType
5252

53+
private let options: HTTPSCallableOptions?
54+
5355
// MARK: - Public Properties
5456

5557
/**
5658
* The timeout to use when calling the function. Defaults to 70 seconds.
5759
*/
5860
@objc open var timeoutInterval: TimeInterval = 70
5961

60-
internal init(functions: Functions, name: String) {
62+
internal init(functions: Functions, name: String, options: HTTPSCallableOptions? = nil) {
6163
self.functions = functions
64+
self.options = options
6265
endpoint = .name(name)
6366
}
6467

65-
internal init(functions: Functions, url: URL) {
68+
internal init(functions: Functions, url: URL, options: HTTPSCallableOptions? = nil) {
6669
self.functions = functions
70+
self.options = options
6771
endpoint = .url(url)
6872
}
6973

@@ -105,11 +109,13 @@ open class HTTPSCallable: NSObject {
105109
case let .name(name):
106110
functions.callFunction(name: name,
107111
withObject: data,
112+
options: options,
108113
timeout: timeoutInterval,
109114
completion: callback)
110115
case let .url(url):
111116
functions.callFunction(url: url,
112117
withObject: data,
118+
options: options,
113119
timeout: timeoutInterval,
114120
completion: callback)
115121
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
17+
/// Configuration options for a ``HTTPSCallable`` instance.
18+
@objc(FIRHTTPSCallableOptions) public class HTTPSCallableOptions: NSObject {
19+
/// Whether or not to protect the callable function with a limited-use App Check token.
20+
@objc public let requireLimitedUseAppCheckTokens: Bool
21+
22+
/// Designated intializer.
23+
/// - Parameter requireLimitedUseAppCheckTokens: A boolean used to decide whether or not to
24+
/// protect the callable function with a limited use App Check token.
25+
@objc public init(requireLimitedUseAppCheckTokens: Bool) {
26+
self.requireLimitedUseAppCheckTokens = requireLimitedUseAppCheckTokens
27+
}
28+
}

0 commit comments

Comments
 (0)