diff --git a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FirebaseFunctionsPlugin.swift b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FirebaseFunctionsPlugin.swift index 1945fd297764..0e679295c8c7 100644 --- a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FirebaseFunctionsPlugin.swift +++ b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FirebaseFunctionsPlugin.swift @@ -99,6 +99,11 @@ public class FirebaseFunctionsPlugin: NSObject, FLTFirebasePluginProtocol, Flutt let functions = Functions.functions(app: app, region: region ?? "") + #if DEBUG + functions.allowInsecureTokenAttachment = + arguments["allowInsecureTokenAttachment"] as? Bool ?? false + #endif + if let origin, !origin.isEmpty, let url = URL(string: origin), let host = url.host, diff --git a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FunctionsStreamHandler.swift b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FunctionsStreamHandler.swift index 7d2ce08e76cc..dbf12e83156b 100644 --- a/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FunctionsStreamHandler.swift +++ b/packages/cloud_functions/cloud_functions/ios/cloud_functions/Sources/cloud_functions/FunctionsStreamHandler.swift @@ -54,6 +54,11 @@ class FunctionsStreamHandler: NSObject, FlutterStreamHandler { let timeout = arguments["timeout"] as? Double let limitedUseAppCheckToken = arguments["limitedUseAppCheckToken"] as? Bool ?? false + #if DEBUG + functions.allowInsecureTokenAttachment = + arguments["allowInsecureTokenAttachment"] as? Bool ?? false + #endif + if let origin, let url = URL(string: origin), let host = url.host, diff --git a/packages/cloud_functions/cloud_functions/lib/src/firebase_functions.dart b/packages/cloud_functions/cloud_functions/lib/src/firebase_functions.dart index aef9a37c036a..348384e52c2f 100644 --- a/packages/cloud_functions/cloud_functions/lib/src/firebase_functions.dart +++ b/packages/cloud_functions/cloud_functions/lib/src/firebase_functions.dart @@ -59,6 +59,33 @@ class FirebaseFunctions extends FirebasePlugin { String? _origin; + /// Whether Auth, FCM, and App Check tokens may be sent over plain HTTP to a + /// non-loopback host. + /// + /// This mirrors `Functions.allowInsecureTokenAttachment` on the Firebase + /// Apple SDK. Set it before calling a function on a local emulator from a + /// physical device: + /// + /// ```dart + /// FirebaseFunctions.instance.allowInsecureTokenAttachment = true; + /// FirebaseFunctions.instance.useFunctionsEmulator('192.168.1.10', 5001); + /// ``` + /// + /// Only Apple debug builds honor this value. `flutter run` is a debug build. + /// Profile and release builds compile the Apple SDK without the property, so + /// the value is ignored there. Android and web already attach these tokens, + /// so the value has no effect on those platforms, and setting it to `false` + /// does not withhold tokens on Android or web. + /// + /// Sending tokens over plain HTTP exposes them on the local network. Enable + /// this only for a local emulator. + bool get allowInsecureTokenAttachment => + delegate.allowInsecureTokenAttachment; + + set allowInsecureTokenAttachment(bool value) { + delegate.allowInsecureTokenAttachment = value; + } + /// A reference to the Callable HTTPS trigger with the given name. /// /// Should be the name of the Callable function in Firebase diff --git a/packages/cloud_functions/cloud_functions/test/firebase_functions_test.dart b/packages/cloud_functions/cloud_functions/test/firebase_functions_test.dart index b3a870044019..b2d73c35ad49 100644 --- a/packages/cloud_functions/cloud_functions/test/firebase_functions_test.dart +++ b/packages/cloud_functions/cloud_functions/test/firebase_functions_test.dart @@ -175,5 +175,32 @@ void main() { expect(delegate.options.timeout.inSeconds, equals(1337)); }); }); + + group('.allowInsecureTokenAttachment', () { + test('defaults to false and writes through to the delegate', () { + final functions = + FirebaseFunctions.instanceFor(region: 'allow-insecure-token'); + expect(functions.allowInsecureTokenAttachment, isFalse); + expect(functions.delegate.allowInsecureTokenAttachment, isFalse); + + functions.allowInsecureTokenAttachment = true; + + expect(functions.allowInsecureTokenAttachment, isTrue); + expect(functions.delegate.allowInsecureTokenAttachment, isTrue); + }); + + test('applies when set after the callable is created', () { + final functions = FirebaseFunctions.instanceFor( + region: 'allow-insecure-token-after-callable'); + final callable = functions.httpsCallable('foo'); + + functions.allowInsecureTokenAttachment = true; + + expect( + callable.delegate.functions.allowInsecureTokenAttachment, + isTrue, + ); + }); + }); }); } diff --git a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart index 41f51be90557..e7e1b0ffe5c1 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart +++ b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/method_channel_https_callable.dart @@ -48,6 +48,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform { 'timeout': options.timeout.inMilliseconds, 'parameters': parameters, 'limitedUseAppCheckToken': options.limitedUseAppCheckToken, + 'allowInsecureTokenAttachment': functions.allowInsecureTokenAttachment, }); return _convertNested(result); @@ -76,6 +77,7 @@ class MethodChannelHttpsCallable extends HttpsCallablePlatform { 'origin': origin, 'parameters': parameters, 'limitedUseAppCheckToken': options.limitedUseAppCheckToken, + 'allowInsecureTokenAttachment': functions.allowInsecureTokenAttachment, 'timeout': options.timeout.inMilliseconds, }; yield* channel.receiveBroadcastStream(eventData).map(_convertNested); diff --git a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/platform_interface/platform_interface_firebase_functions.dart b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/platform_interface/platform_interface_firebase_functions.dart index d9eac236d341..5738ff021b93 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/lib/src/platform_interface/platform_interface_firebase_functions.dart +++ b/packages/cloud_functions/cloud_functions_platform_interface/lib/src/platform_interface/platform_interface_firebase_functions.dart @@ -39,6 +39,12 @@ abstract class FirebaseFunctionsPlatform extends PlatformInterface { /// The region for the HTTPS trigger, such as "us-central1". final String region; + /// Whether Auth, FCM, and App Check tokens may be sent over plain HTTP to a + /// non-loopback host. + /// + /// Only Apple debug builds consult this value. Android and web ignore it. + bool allowInsecureTokenAttachment = false; + /// The current default [FirebaseFunctionsPlatform] instance. /// /// It will always default to [MethodChannelFirebaseFunctions] diff --git a/packages/cloud_functions/cloud_functions_platform_interface/test/method_channel/method_channel_https_callable_test.dart b/packages/cloud_functions/cloud_functions_platform_interface/test/method_channel/method_channel_https_callable_test.dart index 50c92849d903..421592b25fce 100644 --- a/packages/cloud_functions/cloud_functions_platform_interface/test/method_channel/method_channel_https_callable_test.dart +++ b/packages/cloud_functions/cloud_functions_platform_interface/test/method_channel/method_channel_https_callable_test.dart @@ -27,12 +27,15 @@ void main() { dynamic kParameters = {'foo': 'bar'}; HttpsCallableOptions kOptions = HttpsCallableOptions(); String kPlatformExceptionMessage = 'Mock platform exception thrown'; + Map? lastCallArguments; group('$MethodChannelHttpsCallable', () { setUpAll(() async { FirebaseApp app = await Firebase.initializeApp(); - TestCloudFunctionsHostApi.setUp(_TestCloudFunctionsHostApi(() async { + TestCloudFunctionsHostApi.setUp( + _TestCloudFunctionsHostApi((arguments) async { + lastCallArguments = arguments; if (mockExceptionThrown) { throw Exception(); } else if (mockPlatformExceptionThrown) { @@ -56,6 +59,8 @@ void main() { setUp(() async { mockPlatformExceptionThrown = false; mockExceptionThrown = false; + lastCallArguments = null; + functions!.allowInsecureTokenAttachment = false; httpsCallable!.options = kOptions; }); @@ -92,6 +97,17 @@ void main() { }); group('call', () { + test('sends allowInsecureTokenAttachment, defaulting to false', () async { + await httpsCallable!.call(); + + expect(lastCallArguments?['allowInsecureTokenAttachment'], isFalse); + + functions!.allowInsecureTokenAttachment = true; + await httpsCallable!.call(); + + expect(lastCallArguments?['allowInsecureTokenAttachment'], isTrue); + }); + test('converts maps nested in lists', () async { final originalParameters = kParameters; addTearDown(() => kParameters = originalParameters); @@ -120,16 +136,48 @@ void main() { await testExceptionHandling('PLATFORM', httpsCallable!.call); }); }); + + group('stream', () { + test('sends allowInsecureTokenAttachment on the event channel', () async { + functions!.allowInsecureTokenAttachment = true; + const channelName = 'plugins.flutter.io/firebase_functions/test_name_0'; + Object? listenedArguments; + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + messenger.setMockMethodCallHandler(const MethodChannel(channelName), + (call) async { + if (call.method == 'listen') { + listenedArguments = call.arguments; + } + return null; + }); + addTearDown(() { + messenger.setMockMethodCallHandler( + const MethodChannel(channelName), null); + }); + + final subscription = httpsCallable!.stream(null).listen((_) {}); + await Future.delayed(Duration.zero); + await subscription.cancel(); + + expect(listenedArguments, isA()); + expect( + (listenedArguments! as Map)['allowInsecureTokenAttachment'], + isTrue, + ); + }); + }); }); } class _TestCloudFunctionsHostApi implements TestCloudFunctionsHostApi { _TestCloudFunctionsHostApi(this.callHandler); - final Future Function() callHandler; + final Future Function(Map arguments) callHandler; @override - Future call(Map arguments) => callHandler(); + Future call(Map arguments) => + callHandler(arguments); @override Future registerEventChannel(Map arguments) async {}