firebase / flutterfire

🔥 A collection of Firebase plugins for Flutter apps.
https://firebase.google.com/docs/flutter/setup
BSD 3-Clause "New" or "Revised" License
8.63k stars 3.95k forks source link

🐛 [firebase_database] Permission denied while user is actually logged in with an early call on iOS #9433

Closed Tom3652 closed 8 months ago

Tom3652 commented 2 years ago

Bug report

Describe the bug

  1. Check if the user is logged
  2. Log the user in if he wasn't
  3. Query some data with .get()
  4. See a permission denied error with security rules set to .read: "auth.uid != null"

Logs :

IOS :

Installing and launching...
iOS Observatory not discovered after 30 seconds. This is taking much longer than expected...
(lldb) 2022-08-26 18:52:20.864154+0200 Runner[9095:795529] 9.4.0 - [FirebaseCore][I-COR000005] No app has been configured yet.
Debug service listening on ws://127.0.0.1:58258/vBnwnqYGLEE=/ws
Syncing files to device Now You See Me...
flutter: Is user logged : true
flutter: User is logged : GpLhAk8P94UWtJTls3Yi1TZ5Iho1
9.4.0 - [FirebaseDatabase][I-RDB034005] Firebase Database connection was forcefully killed by the server.  Will not attempt reconnect. Reason: Database lives in a different region. Please change your database URL to https://testproj-25f8d-default-rtdb.europe-west1.firebasedatabase.app
flutter: Error getting data : [firebase_database/permission-denied] Client doesn't have permission to access the desired data.

#0      StandardMethodCodec.decodeEnvelope (package:flutter/src/services/message_codecs.dart:607:7)
#1      MethodChannel._invokeMethod (package:flutter/src/services/platform_channel.dart:167:18)
<asynchronous suspension>
#2      MethodChannel.invokeMapMethod (package:flutter/src/services/platform_channel.dart:367:43)
<asynchronous suspension>
#3      MethodChannelQuery.get (package:firebase_database_platform_interface/src/method_channel/method_channel_query.dart:74:22)
<asynchronous suspension>
#4      Query.get (package:firebase_database/src/query.dart:21:27)
<asynchronous suspension>
#5      _TestAppState.initState.<anonymous closure> (package:test_app/main.dart:30:35)
<asynchronous suspension>

Android :

Syncing files to device moto g 8 power...
Restarted application in 1 731ms.
I/flutter (21646): Is user logged : true
I/flutter (21646): User is logged : GpLhAk8P94UWtJTls3Yi1TZ5Iho1
I/flutter (21646): Snapshot : {num: 1}

My database :

Capture d’écran 2022-08-26 à 19 24 31

So according to the logs the bad behavior is only on iOS. In my real life chat app, i have even more delay between the first call but still the permission denied 1 time over 5 runs more or less tested on physical iOS devices.

I have not tested enough on Android but apparently the behavior is correct.

Steps to reproduce

Steps to reproduce the behavior:

  1. Run the sample code
  2. See the logs

Expected behavior

FirebaseDatabase should know that auth.uid != null is true when FirebaseAuth says so to the client.

Sample project

You need a fake user in firebase console : test@gmail.com and password : Azerty1 and also setup security rules :

Capture d’écran 2022-08-26 à 18 52 26

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 MaterialApp(home: TestApp()));
}

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

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

class _TestAppState extends State<TestApp> {

  @override
  void initState() {
    print("Is user logged : ${FirebaseAuth.instance.currentUser != null}");
    FirebaseAuth.instance.userChanges().listen((event) async {
      if (event != null) {
        print("User is logged : ${event.uid}");
        try {
          DataSnapshot snapshot = await FirebaseDatabase.instance.ref().child(
              "test").get();
          print("Snapshot : ${snapshot.value}");
        } catch (error) {
          print("Error getting data : $error");
        }
      }
      else {
        print("User not logged : $event");
        FirebaseAuth.instance.signInWithEmailAndPassword(email: "test@gmail.com", password: "Azerty1");
      }
    });
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return const Scaffold();
  }
}

Note : you may see the :

9.4.0 - [FirebaseDatabase][I-RDB034005] Firebase Database connection was forcefully killed by the server.  Will not attempt reconnect. Reason: Database lives in a different region. Please change your database URL to https://testproj-25f8d-default-rtdb.europe-west1.firebasedatabase.app

But i have tried with :

DataSnapshot snapshot = await FirebaseDatabase.instance.refFromURL("https://testproj-25f8d-default-rtdb.europe-west1.firebasedatabase.app").child("test").get();

And got the exact same logs so ...

Flutter doctor

Run flutter doctor and paste the output below:

Click To Expand ``` Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.0.5, on macOS 12.5.1 21G83 darwin-x64, locale fr-FR) [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 13.4.1) [✓] Chrome - develop for the web [✓] Android Studio (version 2021.2) [✓] VS Code (version 1.70.2) [✓] Connected device (3 available) ! Error: Now You See Me is busy: Fetching debug symbols for Now You See Me. Xcode will continue when Now You See Me is finished. (code -10) [✓] HTTP Host Availability • No issues found! ```

Flutter dependencies

Run flutter pub deps -- --style=compact and paste the output below:

Click To Expand ``` Dart SDK 2.17.6 Flutter SDK 3.0.5 test_app 1.0.0+1 dependencies: - cupertino_icons 1.0.5 - firebase_auth 3.7.0 [firebase_auth_platform_interface firebase_auth_web firebase_core firebase_core_platform_interface flutter meta] - firebase_core 1.21.1 [firebase_core_platform_interface firebase_core_web flutter meta] - firebase_database 9.1.3 [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 1.0.4 [lints] - flutter_test 0.0.0 [flutter test_api path fake_async clock stack_trace vector_math async boolean_selector characters charcode collection matcher material_color_utilities meta source_span stream_channel string_scanner term_glyph] transitive dependencies: - async 2.8.2 [collection meta] - boolean_selector 2.1.0 [source_span string_scanner] - characters 1.2.0 - charcode 1.3.1 - clock 1.1.0 - collection 1.16.0 - fake_async 1.3.0 [clock collection] - firebase_auth_platform_interface 6.6.0 [collection firebase_core flutter meta plugin_platform_interface] - firebase_auth_web 4.3.0 [firebase_auth_platform_interface firebase_core firebase_core_web flutter flutter_web_plugins http_parser intl js meta] - firebase_core_platform_interface 4.5.1 [collection flutter flutter_test meta plugin_platform_interface] - firebase_core_web 1.7.2 [firebase_core_platform_interface flutter flutter_web_plugins js meta] - firebase_database_platform_interface 0.2.2+3 [collection firebase_core flutter meta plugin_platform_interface] - firebase_database_web 0.2.1+5 [firebase_core firebase_core_web firebase_database_platform_interface flutter flutter_web_plugins js] - flutter_web_plugins 0.0.0 [flutter js characters collection material_color_utilities meta vector_math] - http_parser 4.0.1 [collection source_span string_scanner typed_data] - intl 0.17.0 [clock path] - js 0.6.4 - lints 1.0.1 - matcher 0.12.11 [stack_trace] - material_color_utilities 0.1.4 - meta 1.7.0 - path 1.8.1 - plugin_platform_interface 2.1.2 [meta] - sky_engine 0.0.99 - source_span 1.8.2 [collection path term_glyph] - stack_trace 1.10.0 [path] - stream_channel 2.1.0 [async] - string_scanner 1.1.0 [charcode source_span] - term_glyph 1.2.0 - test_api 0.4.9 [async boolean_selector collection meta source_span stack_trace stream_channel string_scanner term_glyph matcher] - typed_data 1.3.1 [collection] - vector_math 2.1.2 ```

darshankawar commented 2 years ago

@Tom3652 Thanks for the detailed report and code sample. Looking at await Firebase.initializeApp();, I am assuming that you are using googleservice-info.plist / google-services.json` file to initialize your firebase app.

If so, does it contain "firebase_url": with correct value ? If not, it could be the reason why you might be seeing Firebase Database connection was forcefully killed by the server. Will not attempt reconnect. Reason: Database lives in a different region. Please change your database URL to https://testproj-25f8d-default-rtdb.europe-west1.firebasedatabase.app.

If the firebase_url entry isn't there and if you add it, you may need to do a flutter clean to see if it works.

If above isn't the case, can you try with something like below security rule that indicates that app will allow accessing the data only if you are logged in:

{
  "rules": {
    "users":{
      "$uid": {
        ".read": "$uid === auth.uid",
        ".write": "$uid === auth.uid"
      }
    }
}
Tom3652 commented 2 years ago

Hi @darshankawar you are right i can't reproduce it by updating the googleservice-info.plist. However in my real app this is happening sometimes and this is a real issue :(

I have the logs in Crashlytics and i have seen which method is causing this, this is really the same as i did in the example above and 1 time / 10 my users tell me it doesn't fetch data and i see the permission denied in the logs.

darshankawar commented 2 years ago

Did you get a chance to try with the security rules I shared above to see if it helps ?

Tom3652 commented 2 years ago

Thanks for your answer.

I can't actually try with these values in my real app where the problem appears, here are my security rules :

{
  "rules": {
    ".read": "auth.uid != null",
    "shards": {
      ".indexOn": ["full"],
      "$shardName": {
        "num": {
          ".write": "auth.uid != null"
        },
        "$other": {
          ".write": false
        }
      }
    },
  }
}

The second line causes the issue : ".read": "auth.uid != null", The above rules apply to the default firebase RTDB instance.

They are meant for anyone logged in to be able to read the public infos. I can't structure this part of my DB with user ids :/

However, my security rules for my chat app (other shards of RTDB i have got) are much more complexe and all working fine, especially the ones with auth.uid == $userID as the structure you mentioned.

I have the followings on all of my shards (other RTDB instances) :

{
  "rules": {
    "users": {
      "$userID": {
        ".indexOn": ["at"],
        ".write": "auth.uid != null",
        ".read": "auth.uid == $userID",
      }
    },
    "conversations": {
      ".read": "auth.uid != null",
      ".write": "auth.uid != null"
    },
    ...
}    

The first block users never caused any issue and are called at the same time and has a structure for .read similar to what you suggested.

But i just saw 2 new crashlytics reports about the conversations block, that indeed has again .read: "auth.uid != null" in the rules.

darshankawar commented 2 years ago

Please check below links and see if they help.

https://stackoverflow.com/a/70319400 https://groups.google.com/g/firebase-talk/c/JRScicQnARc?pli=1

Tom3652 commented 2 years ago

The SO link is exactly the issue i am facing.
The guy there mentions that it can comes from a poor network signal strength, which actually matches my tests.

In that case, the RTDB should not throw a permission-denied error... But now this is more clear of what is happening, so i will implement a retry on the method that "should never" be in permission denied in my app, like if it was a network error.

The workaround is quite ugly but i prefer doing so than nothing.

Tom3652 commented 2 years ago

I confirm that i am avoiding this behavior in my app, but i am doing this ugly workaround :

DataSnapshot dataShard = await FirebaseDatabase.instance
              .ref()
              .child("shards")
              .orderByChild("full")
              .equalTo(false)
              .limitToFirst(1)
              .get();
          Map<String, dynamic> shard =
              Map<String, dynamic>.from(dataShard.value as Map);
          _shardName = shard.keys.first;
          LogManager.d("Shard name : $_shardName");
        } catch (error) {
          if (error.toString().contains("denied")) {
            LogManager.e("Permission denied while fetching shards, retrying...");
            await Future.delayed(const Duration(seconds: 2));
            ensureInitialized();
          }
        }

This is fixing my current issue as the permission-denied doesn't occur repeatedly but it's very bad to code like this... I have kept a counter in a later version to check if it's denied 3 times i stop the recursive function.

There is a real bug in the RTDB probably similar to the one i had created long time ago #8220 where the DB was not waiting for backend connection before querying. I suspect the same behavior over the DB checking the rules correctly or not throwing the correct error (if it's actually an internet / delay error as it seems, the DB should throw the SocketException, not a permission-denied).

darshankawar commented 2 years ago

Thanks for the update. Keeping this issue open for further insights from the team.

DanGalkin commented 1 year ago

@Tom3652 Hey, I've just dealt with the same issue of [permission-denied] with a weak connection.

The problem vanished after proper GoogleService-info.plist importing to the Runner in XCode before build. Proper importing is described in the video I found in SO thread: https://stackoverflow.com/questions/49245019/flutter-firebase-database-permission-denied-after-successful-authentication

Tom3652 commented 1 year ago

Hey @DanGalkin thanks for suggesting a solution, however i see that the video is 6 years old and this is the first setup i have made :/ Today i am importing the GoogleService-info.plist by using flutterfire configure if it doesn't exist in the project.

I do not see anything different in the video from what i did when i did it manually back then :(

Could you tell me if i missed something that would help getting rid of this behavior ? I mean this issue is still present in my released app in production today, i have Crashlytics logs that tells me permission denied but i have setup a retry listener for few times and it's working eventually.

I don't really see how the GoogleService-info.plist file plays a role here

DanGalkin commented 1 year ago

@Tom3652 Well, my initial mistake was importing .plist file by dragging and dropping to the Runner instead of Runner\Runner Снимок экрана 2023-05-17 в 18 24 20 This detail was my issue, maybe it will help you too.

Tom3652 commented 1 year ago

Oh alright thanks for specifying, yes indeed you are right it will help others if any have problems with that !

On my side i had already set it up correctly in Runner/Runner, i am still not sure how this is related to this issue but rather with a general permission denied issue (with firebase_auth) if the GoogleService-Info can't be read, no firebase products should work.

In this issue, i point out the fact that the permission denied on weak connection happens during the first few seconds the app starts, then start working 😅

DanGalkin commented 1 year ago

It somehow worked in my app with improper .plist import and I had a similar behavior you described. (Maybe XCode used the file I uploaded directly to the Runner folder in my project folder) But anyway, wish you to find a solution to the problem.

Almis90 commented 1 year ago

I had the same error and for me the solution was to enable Outgoing Connections (Client) in Signing & Capabilities

CyberWake commented 9 months ago

Outgoing Connections (Client) in Signing & Capabilities

Hey can you elaborate on this. I was unable to find this in signing capabilities.

russellwheatley commented 8 months ago

This isn't an issue with FlutterFire, it is likely at the level of the native iOS SDK. If you're still experiencing this pretty stale issue, I'd advise opening an issue on the native iOS SDK (providing a native reproduction).