Skip to content

Commit 8f95461

Browse files
authored
feat(cloud_functions): expose allowInsecureTokenAttachment for Apple debug builds (#18734)
* feat(cloud_functions): expose allowInsecureTokenAttachment for Apple debug builds Physical devices cannot send Auth and App Check tokens to a Functions emulator on a LAN address unless the Apple SDK debug opt-in is set. * style(cloud_functions): format the insecure token attachment test
1 parent efa14e0 commit 8f95461

7 files changed

Lines changed: 123 additions & 3 deletions

File tree

‎packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FirebaseFunctionsPlugin.swift‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ public class FirebaseFunctionsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt
9999

100100
let functions = Functions.functions(app: app, region: region ?? "")
101101

102+
#if DEBUG
103+
functions.allowInsecureTokenAttachment =
104+
arguments["allowInsecureTokenAttachment"] as? Bool ?? false
105+
#endif
106+
102107
if let origin, !origin.isEmpty,
103108
let url = URL(string: origin),
104109
let host = url.host,

‎packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FunctionsStreamHandler.swift‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ class FunctionsStreamHandler: NSObject, FlutterStreamHandler {
5454
let timeout = arguments["timeout"] as? Double
5555
let limitedUseAppCheckToken = arguments["limitedUseAppCheckToken"] as? Bool ?? false
5656

57+
#if DEBUG
58+
functions.allowInsecureTokenAttachment =
59+
arguments["allowInsecureTokenAttachment"] as? Bool ?? false
60+
#endif
61+
5762
if let origin,
5863
let url = URL(string: origin),
5964
let host = url.host,

‎packages/cloud_functions/cloud_functions/lib/src/firebase_functions.dart‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,33 @@ class FirebaseFunctions extends FirebasePlugin {
5959

6060
String? _origin;
6161

62+
/// Whether Auth, FCM, and App Check tokens may be sent over plain HTTP to a
63+
/// non-loopback host.
64+
///
65+
/// This mirrors `Functions.allowInsecureTokenAttachment` on the Firebase
66+
/// Apple SDK. Set it before calling a function on a local emulator from a
67+
/// physical device:
68+
///
69+
/// ```dart
70+
/// FirebaseFunctions.instance.allowInsecureTokenAttachment = true;
71+
/// FirebaseFunctions.instance.useFunctionsEmulator('192.168.1.10', 5001);
72+
/// ```
73+
///
74+
/// Only Apple debug builds honor this value. `flutter run` is a debug build.
75+
/// Profile and release builds compile the Apple SDK without the property, so
76+
/// the value is ignored there. Android and web already attach these tokens,
77+
/// so the value has no effect on those platforms, and setting it to `false`
78+
/// does not withhold tokens on Android or web.
79+
///
80+
/// Sending tokens over plain HTTP exposes them on the local network. Enable
81+
/// this only for a local emulator.
82+
bool get allowInsecureTokenAttachment =>
83+
delegate.allowInsecureTokenAttachment;
84+
85+
set allowInsecureTokenAttachment(bool value) {
86+
delegate.allowInsecureTokenAttachment = value;
87+
}
88+
6289
/// A reference to the Callable HTTPS trigger with the given name.
6390
///
6491
/// Should be the name of the Callable function in Firebase

‎packages/cloud_functions/cloud_functions/test/firebase_functions_test.dart‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,5 +175,32 @@ void main() {
175175
expect(delegate.options.timeout.inSeconds, equals(1337));
176176
});
177177
});
178+
179+
group('.allowInsecureTokenAttachment', () {
180+
test('defaults to false and writes through to the delegate', () {
181+
final functions =
182+
FirebaseFunctions.instanceFor(region: 'allow-insecure-token');
183+
expect(functions.allowInsecureTokenAttachment, isFalse);
184+
expect(functions.delegate.allowInsecureTokenAttachment, isFalse);
185+
186+
functions.allowInsecureTokenAttachment = true;
187+
188+
expect(functions.allowInsecureTokenAttachment, isTrue);
189+
expect(functions.delegate.allowInsecureTokenAttachment, isTrue);
190+
});
191+
192+
test('applies when set after the callable is created', () {
193+
final functions = FirebaseFunctions.instanceFor(
194+
region: 'allow-insecure-token-after-callable');
195+
final callable = functions.httpsCallable('foo');
196+
197+
functions.allowInsecureTokenAttachment = true;
198+
199+
expect(
200+
callable.delegate.functions.allowInsecureTokenAttachment,
201+
isTrue,
202+
);
203+
});
204+
});
178205
});
179206
}

‎packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
4848
'timeout': options.timeout.inMilliseconds,
4949
'parameters': parameters,
5050
'limitedUseAppCheckToken': options.limitedUseAppCheckToken,
51+
'allowInsecureTokenAttachment': functions.allowInsecureTokenAttachment,
5152
});
5253

5354
return _convertNested(result);
@@ -76,6 +77,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform {
7677
'origin': origin,
7778
'parameters': parameters,
7879
'limitedUseAppCheckToken': options.limitedUseAppCheckToken,
80+
'allowInsecureTokenAttachment': functions.allowInsecureTokenAttachment,
7981
'timeout': options.timeout.inMilliseconds,
8082
};
8183
yield* channel.receiveBroadcastStream(eventData).map(_convertNested);

‎packages/cloud_functions/cloud_functions_platform_interface/lib/src/platform_interface/platform_interface_firebase_functions.dart‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ abstract class FirebaseFunctionsPlatform extends PlatformInterface {
3939
/// The region for the HTTPS trigger, such as "us-central1".
4040
final String region;
4141

42+
/// Whether Auth, FCM, and App Check tokens may be sent over plain HTTP to a
43+
/// non-loopback host.
44+
///
45+
/// Only Apple debug builds consult this value. Android and web ignore it.
46+
bool allowInsecureTokenAttachment = false;
47+
4248
/// The current default [FirebaseFunctionsPlatform] instance.
4349
///
4450
/// It will always default to [MethodChannelFirebaseFunctions]

‎packages/cloud_functions/cloud_functions_platform_interface/test/method_channel/method_channel_https_callable_test.dart‎

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,15 @@ void main() {
2727
dynamic kParameters = {'foo': 'bar'};
2828
HttpsCallableOptions kOptions = HttpsCallableOptions();
2929
String kPlatformExceptionMessage = 'Mock platform exception thrown';
30+
Map<String, Object?>? lastCallArguments;
3031

3132
group('$MethodChannelHttpsCallable', () {
3233
setUpAll(() async {
3334
FirebaseApp app = await Firebase.initializeApp();
3435

35-
TestCloudFunctionsHostApi.setUp(_TestCloudFunctionsHostApi(() async {
36+
TestCloudFunctionsHostApi.setUp(
37+
_TestCloudFunctionsHostApi((arguments) async {
38+
lastCallArguments = arguments;
3639
if (mockExceptionThrown) {
3740
throw Exception();
3841
} else if (mockPlatformExceptionThrown) {
@@ -56,6 +59,8 @@ void main() {
5659
setUp(() async {
5760
mockPlatformExceptionThrown = false;
5861
mockExceptionThrown = false;
62+
lastCallArguments = null;
63+
functions!.allowInsecureTokenAttachment = false;
5964
httpsCallable!.options = kOptions;
6065
});
6166

@@ -92,6 +97,17 @@ void main() {
9297
});
9398

9499
group('call', () {
100+
test('sends allowInsecureTokenAttachment, defaulting to false', () async {
101+
await httpsCallable!.call();
102+
103+
expect(lastCallArguments?['allowInsecureTokenAttachment'], isFalse);
104+
105+
functions!.allowInsecureTokenAttachment = true;
106+
await httpsCallable!.call();
107+
108+
expect(lastCallArguments?['allowInsecureTokenAttachment'], isTrue);
109+
});
110+
95111
test('converts maps nested in lists', () async {
96112
final originalParameters = kParameters;
97113
addTearDown(() => kParameters = originalParameters);
@@ -120,16 +136,48 @@ void main() {
120136
await testExceptionHandling('PLATFORM', httpsCallable!.call);
121137
});
122138
});
139+
140+
group('stream', () {
141+
test('sends allowInsecureTokenAttachment on the event channel', () async {
142+
functions!.allowInsecureTokenAttachment = true;
143+
const channelName = 'plugins.flutter.io/firebase_functions/test_name_0';
144+
Object? listenedArguments;
145+
final messenger =
146+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
147+
messenger.setMockMethodCallHandler(const MethodChannel(channelName),
148+
(call) async {
149+
if (call.method == 'listen') {
150+
listenedArguments = call.arguments;
151+
}
152+
return null;
153+
});
154+
addTearDown(() {
155+
messenger.setMockMethodCallHandler(
156+
const MethodChannel(channelName), null);
157+
});
158+
159+
final subscription = httpsCallable!.stream(null).listen((_) {});
160+
await Future<void>.delayed(Duration.zero);
161+
await subscription.cancel();
162+
163+
expect(listenedArguments, isA<Map>());
164+
expect(
165+
(listenedArguments! as Map)['allowInsecureTokenAttachment'],
166+
isTrue,
167+
);
168+
});
169+
});
123170
});
124171
}
125172

126173
class _TestCloudFunctionsHostApi implements TestCloudFunctionsHostApi {
127174
_TestCloudFunctionsHostApi(this.callHandler);
128175

129-
final Future<Object?> Function() callHandler;
176+
final Future<Object?> Function(Map<String, Object?> arguments) callHandler;
130177

131178
@override
132-
Future<Object?> call(Map<String, Object?> arguments) => callHandler();
179+
Future<Object?> call(Map<String, Object?> arguments) =>
180+
callHandler(arguments);
133181

134182
@override
135183
Future<void> registerEventChannel(Map<String, Object> arguments) async {}

0 commit comments

Comments
 (0)