Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[firebase_database]: Listener throws permission denied after sign out despite awaiting the StreamSubscription to be canceled before #16726

Open
1 task done
Tom3652 opened this issue Nov 19, 2024 · 1 comment
Labels
Needs Attention This issue needs maintainer attention. platform: android Issues / PRs which are specifically for Android. platform: ios Issues / PRs which are specifically for iOS. plugin: database type: bug Something isn't working

Comments

@Tom3652
Copy link

Tom3652 commented Nov 19, 2024

Is there an existing issue for this?

  • I have searched the existing issues.

Which plugins are affected?

Database

Which platforms are affected?

Android, iOS

Description

When you cancel the StreamSubscription listener of a Firebase and sign out right after, there is a chance to get a race condition where FirebaseDatabase throws a permission denied error when the listener should already be cancelled.

Actual results : In my real app, i am cancelling all the listeners i have to my FirebaseDatabase instance, then i sign out the user when he clicks on Sign out.
That works fine most of the time, but among my few thousands active users, i have few hundreds of "Permission denied" logs in Crashlytics.

Moreover, i have managed to reproduce it on my local machine, this happens often enough on both Android and iOS to be noticeable.

Expected results : When calling await _listener?.cancel() on a FirebaseDatabase.instance.ref().onValue.listen((d) {}) StreamSubscription, i would like the stream to be canceled at 100% when the await completes, which would avoid all these scenarios with permission denied errors.

Reproducing the issue

  1. Run the sample code
  2. Write a read rule to auth.uid != null in the Firebase console on the testing node
  3. Play around with the sign in / sign out but always in the correct order until you saw the 0
  4. See the race condition after some time depending on the "bug"
import 'dart:async';

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  runApp(const TestApp());
}

class TestApp extends StatefulWidget {
  const TestApp({super.key});

  @override
  State<TestApp> createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {
  StreamSubscription? _listener;

  Future<void> register() async {

    _listener =
        FirebaseDatabase.instance.ref().child("test").onValue.listen((d) {
      print(d.snapshot.value);
    });
  }

  Future<void> signIn() async {
    print("Sign in");
    await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: "<your_email>", password: "<your_password>");
    register();
  }

  Future<void> signOut() async {
    print("Sign out !");
    await _listener?.cancel();
    FirebaseAuth.instance.signOut();
  }

  @override
  void initState() {

    signIn();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton(onPressed: () {
                signIn();
              }, child: Text("Sign in")),
              SizedBox(
                height: 30,
              ),
              ElevatedButton(onPressed: () {
                signOut();
              }, child: Text("Sign out"))
            ],
          ),
        ),
      ),
    );
  }
}

Firebase Core version

3.8.0

Flutter Version

3.24.5

Relevant Log Output

flutter: Sign in
flutter: 0
flutter: Sign out !
11.4.0 - [FirebaseDatabase][I-RDB038012] Listener at /test failed: permission_denied
[ERROR:flutter/runtime/dart_vm_initializer.cc(41)] Unhandled Exception: [firebase_database/permission-denied] Client doesn't have permission to access the desired data.
#0      EventChannelExtension.receiveGuardedBroadcastStream (package:_flutterfire_internals/src/exception.dart:67:43)
#1      MethodChannelQuery.observe (package:firebase_database_platform_interface/src/method_channel/method_channel_query.dart:57:39)
<asynchronous suspension>
#2      _ForwardingStreamSubscription._handleData (dart:async/stream_pipe.dart:152:3)
<asynchronous suspension>

Flutter dependencies

Expand Flutter dependencies snippet
Dart SDK 3.5.4
Flutter SDK 3.24.5
test_database 1.0.0+1

dependencies:
- firebase_auth 5.3.3 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta]
- firebase_core 3.8.0 [firebase_core_platform_interface firebase_core_web flutter meta]
- firebase_database 11.1.6 [firebase_core firebase_core_platform_interface firebase_database_platform_interface firebase_database_web flutter]
- flutter 0.0.0 [characters collection material_color_utilities meta vector_math sky_engine]

dev dependencies:
- flutter_lints 4.0.0 [lints]
- flutter_test 0.0.0 [flutter test_api matcher path fake_async clock stack_trace vector_math leak_tracker_flutter_testing async boolean_selector characters collection leak_tracker leak_tracker_testing material_color_utilities meta source_span stream_channel string_scanner term_glyph vm_service]

transitive dependencies:
- _flutterfire_internals 1.3.46 [collection firebase_core firebase_core_platform_interface flutter meta]
- async 2.11.0 [collection meta]
- boolean_selector 2.1.1 [source_span string_scanner]
- characters 1.3.0
- clock 1.1.1
- collection 1.18.0
- fake_async 1.3.1 [clock collection]
- firebase_auth_platform_interface 7.4.9 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface]
- firebase_auth_web 5.13.4 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser meta web]
- firebase_core_platform_interface 5.3.0 [collection flutter flutter_test meta plugin_platform_interface]
- firebase_core_web 2.18.1 [firebase_core_platform_interface flutter flutter_web_plugins meta web]
- firebase_database_platform_interface 0.2.5+46 [_flutterfire_internals collection firebase_core flutter meta plugin_platform_interface]
- firebase_database_web 0.2.6+4 [collection firebase_core firebase_core_web firebase_database_platform_interface flutter flutter_web_plugins]
- flutter_web_plugins 0.0.0 [flutter characters collection material_color_utilities meta vector_math]
- http_parser 4.0.2 [collection source_span string_scanner typed_data]
- leak_tracker 10.0.5 [clock collection meta path vm_service]
- leak_tracker_flutter_testing 3.0.5 [flutter leak_tracker leak_tracker_testing matcher meta]
- leak_tracker_testing 3.0.1 [leak_tracker matcher meta]
- lints 4.0.0
- matcher 0.12.16+1 [async meta stack_trace term_glyph test_api]
- material_color_utilities 0.11.1 [collection]
- meta 1.15.0
- path 1.9.0
- plugin_platform_interface 2.1.8 [meta]
- sky_engine 0.0.99
- source_span 1.10.0 [collection path term_glyph]
- stack_trace 1.11.1 [path]
- stream_channel 2.1.2 [async]
- string_scanner 1.2.0 [source_span]
- term_glyph 1.2.1
- test_api 0.7.2 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph]
- typed_data 1.4.0 [collection]
- vector_math 2.1.4
- vm_service 14.2.5
- web 1.1.0

Additional context and comments

No response

@Tom3652 Tom3652 added Needs Attention This issue needs maintainer attention. type: bug Something isn't working labels Nov 19, 2024
@SelaseKay SelaseKay added plugin: database platform: android Issues / PRs which are specifically for Android. platform: ios Issues / PRs which are specifically for iOS. labels Nov 20, 2024
@SelaseKay
Copy link
Contributor

Hi @Tom3652, thanks for the report. I'm able to reproduce this issue.

Reproducible code

Reproducible code
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';
import 'package:flutter/foundation.dart' show defaultTargetPlatform, kIsWeb;
import 'package:flutter/material.dart';
import 'package:firebase_auth/firebase_auth.dart';

import 'firebase_options.dart';

// Change to false to use live database instance.
const USE_DATABASE_EMULATOR = false;
// The port we've set the Firebase Database emulator to run on via the
// `firebase.json` configuration file.
const emulatorPort = 9000;
// Android device emulators consider localhost of the host machine as 10.0.2.2
// so let's use that if running on Android.
final emulatorHost =
    (!kIsWeb && defaultTargetPlatform == TargetPlatform.android)
        ? '10.0.2.2'
        : 'localhost';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  if (USE_DATABASE_EMULATOR) {
    FirebaseDatabase.instance.useDatabaseEmulator(emulatorHost, emulatorPort);
  }

  runApp(
    const MaterialApp(
      title: 'Flutter Database Example',
      home: TestApp(),
    ),
  );
}

class TestApp extends StatefulWidget {
  const TestApp({super.key});

  @override
  State<TestApp> createState() => _TestAppState();
}

class _TestAppState extends State<TestApp> {
  StreamSubscription? _listener;

  Future<void> register() async {

    _listener =
        FirebaseDatabase.instance.ref().child("test_path").onValue.listen((d) {
      print("Listner value: ${d.snapshot.value}");
    });
  }

  Future<void> signIn() async {
    print("Sign in");
    await FirebaseAuth.instance.signInWithEmailAndPassword(
        email: "your-email", password: "your-password");
    register();
  }

  Future<void> signOut() async {
    print("Sign out !");
    await _listener?.cancel();
    await FirebaseAuth.instance.signOut();
  }

  @override
  void initState() {

    signIn();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              ElevatedButton(onPressed: () {
                signIn();
              }, child: Text("Sign in")),
              SizedBox(
                height: 30,
              ),
              ElevatedButton(onPressed: () {
                signOut();
              }, child: Text("Sign out"))
            ],
          ),
        ),
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key}) : super(key: key);

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;
  late DatabaseReference _counterRef;
  late DatabaseReference _messagesRef;
  late StreamSubscription<DatabaseEvent> _counterSubscription;
  late StreamSubscription<DatabaseEvent> _messagesSubscription;
  bool _anchorToBottom = false;

  String _kTestKey = 'Hello';
  String _kTestValue = 'world!';
  FirebaseException? _error;
  bool initialized = false;

  @override
  void initState() {
    init();
    super.initState();
  }

  Future<void> init() async {
    _counterRef = FirebaseDatabase.instance.ref('counter');

    final database = FirebaseDatabase.instance;

    _messagesRef = database.ref('messages');

    database.setLoggingEnabled(false);

    if (!kIsWeb) {
      database.setPersistenceEnabled(true);
      database.setPersistenceCacheSizeBytes(10000000);
    }

    if (!kIsWeb) {
      await _counterRef.keepSynced(true);
    }

    setState(() {
      initialized = true;
    });

    try {
      final counterSnapshot = await _counterRef.get();

      print(
        'Connected to directly configured database and read '
        '${counterSnapshot.value}',
      );
    } catch (err) {
      print(err);
    }

    _counterSubscription = _counterRef.onValue.listen(
      (DatabaseEvent event) {
        setState(() {
          _error = null;
          _counter = (event.snapshot.value ?? 0) as int;
        });
      },
      onError: (Object o) {
        final error = o as FirebaseException;
        setState(() {
          _error = error;
        });
      },
    );

    final messagesQuery = _messagesRef.limitToLast(10);

    _messagesSubscription = messagesQuery.onChildAdded.listen(
      (DatabaseEvent event) {
        print('Child added: ${event.snapshot.value}');
      },
      onError: (Object o) {
        final error = o as FirebaseException;
        print('Error: ${error.code} ${error.message}');
      },
    );
  }

  @override
  void dispose() {
    super.dispose();
    _messagesSubscription.cancel();
    _counterSubscription.cancel();
  }

  Future<void> _increment() async {
    await _counterRef.set(ServerValue.increment(1));

    await _messagesRef
        .push()
        .set(<String, String>{_kTestKey: '$_kTestValue $_counter'});
  }

  Future<void> _incrementAsTransaction() async {
    try {
      final transactionResult = await _counterRef.runTransaction((mutableData) {
        return Transaction.success((mutableData as int? ?? 0) + 1);
      });

      if (transactionResult.committed) {
        final newMessageRef = _messagesRef.push();
        await newMessageRef.set(<String, String>{
          _kTestKey: '$_kTestValue ${transactionResult.snapshot.value}',
        });
      }
    } on FirebaseException catch (e) {
      print(e.message);
    }
  }

  Future<void> _deleteMessage(DataSnapshot snapshot) async {
    final messageRef = _messagesRef.child(snapshot.key!);
    await messageRef.remove();
  }

  void _setAnchorToBottom(bool? value) {
    if (value == null) {
      return;
    }

    setState(() {
      _anchorToBottom = value;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (!initialized) return Container();

    return Scaffold(
      appBar: AppBar(
        title: const Text('Flutter Database Example'),
      ),
      body: Column(
        children: [
          Flexible(
            child: Center(
              child: _error == null
                  ? Text(
                      'Button tapped $_counter time${_counter == 1 ? '' : 's'}.\n\n'
                      'This includes all devices, ever.',
                    )
                  : Text(
                      'Error retrieving button tap count:\n${_error!.message}',
                    ),
            ),
          ),
          ElevatedButton(
            onPressed: _incrementAsTransaction,
            child: const Text('Increment as transaction'),
          ),
          ListTile(
            leading: Checkbox(
              onChanged: _setAnchorToBottom,
              value: _anchorToBottom,
            ),
            title: const Text('Anchor to bottom'),
          ),
          Flexible(
            child: FirebaseAnimatedList(
              key: ValueKey<bool>(_anchorToBottom),
              query: _messagesRef,
              reverse: _anchorToBottom,
              itemBuilder: (context, snapshot, animation, index) {
                return SizeTransition(
                  sizeFactor: animation,
                  child: ListTile(
                    trailing: IconButton(
                      onPressed: () => _deleteMessage(snapshot),
                      icon: const Icon(Icons.delete),
                    ),
                    title: Text('$index: ${snapshot.value}'),
                  ),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
  • Replace firebase_database example app's main.dart with code above.
  • Run code and play around with sign in/ sign out.
  • Observe the error at some point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Attention This issue needs maintainer attention. platform: android Issues / PRs which are specifically for Android. platform: ios Issues / PRs which are specifically for iOS. plugin: database type: bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants