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

Firestore Emulator Security Rule request.auth is always null even after authed #5072

Closed
mono0926 opened this issue Mar 11, 2020 · 21 comments · Fixed by #5420
Closed

Firestore Emulator Security Rule request.auth is always null even after authed #5072

mono0926 opened this issue Mar 11, 2020 · 21 comments · Fixed by #5420
Assignees
Milestone

Comments

@mono0926
Copy link

mono0926 commented Mar 11, 2020

[REQUIRED] Step 1: Describe your environment

  • Xcode version: 11.3.1
  • Firebase SDK version: 6.19.0
  • Firebase Component: Auth, Firestore
  • Component version: Auth: 6.19.0, Firestore: 6.19.0
  • Installation method: CocoaPods

[REQUIRED] Step 2: Describe the problem

Firestore Emulator Security Rule request.auth is always null even after authed.

I tried it on Android project/emulator, and it works fine, so I suspect it is iOS SDK's bug.

Steps to reproduce:

I created the reproduced project: https://github.com/mono0926/firestore-emulator-sample

1. Prepare Firestore Emulator

Execute this:

git clone https://github.com/mono0926/firestore-emulator-sample.git
cd firestore-emulator-sample/firebase
firebase emulators:start

If you create your own Firebase project, these steps are needed:

Enable Anonymous Auth:
image

Enable Firestore:
image

2. Run iOS application

Execute this:

cd firestore-emulator-sample/iOS/ 
pod install
open EmulatorSample.xcworkspace

And Run app on iOS simulator, and push the button:

image

The error log will be output:

error: Optional(Error Domain=FIRFirestoreErrorDomain Code=7 "
false for 'get' @ L5" UserInfo={NSLocalizedDescription=
false for 'get' @ L5})

Relevant Code:

Security Rule

This security rule allows /users/{userId} get only if request.auth != null

https://github.com/mono0926/firestore-emulator-sample/blob/633cd0d48c8b6eaf228d1fb3f978420df1fc4e6d/firebase/firestore.rules#L5

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow get: if request.auth != null;
    }
  }
}
iOS initialization

At didFinishLaunchingWithOptions, set to connect to Firestore emulator.

https://github.com/mono0926/firestore-emulator-sample/blob/633cd0d48c8b6eaf228d1fb3f978420df1fc4e6d/iOS/EmulatorSample/AppDelegate.swift#L20-L24

        let settings = Firestore.firestore().settings
        settings.host = "localhost:8080"
        settings.isPersistenceEnabled = false
        settings.isSSLEnabled = false
        Firestore.firestore().settings = settings
iOS logic
  1. Auth.auth().signInAnonymously
  2. Firestore.firestore().collection("users").document(uid).getDocument()

https://github.com/mono0926/firestore-emulator-sample/blob/32f5c5bdf59eba8098c3bede21c6da430568b799/iOS/EmulatorSample/ViewController.swift#L19-L39

    @IBAction func buttonDidTap(_ sender: Any) {
        Auth.auth().signInAnonymously { (result, error) in
            assert(error == nil)
            
            guard let result = result else {
                assert(false)
                return;
            }
            
            let uid = result.user.uid
            print(uid)
            Firestore.firestore().collection("users").document(uid).getDocument { (snap, error) in
//                "snap: nil"
                print("snap: \(snap?.data())")
//                error: Optional(Error Domain=FIRFirestoreErrorDomain Code=7 "
//                false for 'get' @ L5" UserInfo={NSLocalizedDescription=
//                false for 'get' @ L5})
                print("error: \(error)")
            }
        }
    }

This should succeed without any errors because the user is authed and firestore security rule only checks request.auth != null and this condition is satisfied.

If setting to connect to emulator codes is commented out, the app will connect to real Firestore backend(same security rule deployed), and it works as expected.

@samtstern
Copy link
Contributor

@mono0926 thank you for the extremely detailed bug report, this feels like a bug but I'm not sure at the moment if it's a bug in the iOS SDK or if it's a bug in the emulator(s).

@alvinthen
Copy link

Anything I can do to help further debugging this? It's a letdown not being able to test my app against the emulator suite

@logananderson
Copy link

I'm running into this issue as well. In order to test iOS against the emulator I have to change rules to allow all (allow read, write: if true;)
This unblocks local testing but not of security features, obviously.

@morganchen12
Copy link
Contributor

@renkelvin can you take a moment to check if this is an SDK bug or an emulator bug?

@renkelvin
Copy link
Contributor

@morganchen12 looks like an issue related to Firestore, not Auth.

@samtstern
Copy link
Contributor

@renkelvin it could be related to both. Firestore needs to ask Auth for the token and Auth needs to provide one, then Firestore sends it along to the emulator and the emulator unpacks it.

I can confirm this works on Android and the Web so it's certainly something within the iOS SDK

@paulb777
Copy link
Member

@wilhuff Any suggestions for how to debug? It's not obvious how to determine where Error::PermissionDenied gets set.

@wilhuff
Copy link
Contributor

wilhuff commented Apr 14, 2020

The Firestore client never sets this error code directly unless we encounter e.g. a filesystem operation with an errno that maps onto PermissionDenied (here). That doesn't seem to be what's happening in this case.

It's definitely possible that Firestore is failing to attach the credentials in some way, though this working in production but not against the emulator seems suspicious.

The best path forward is to enable debug logging in Firestore and look at all the lines relating to the current user. We log when initializing or the current identity changes.

You can observe what Firestore is sending with gRPC tracing. Set these environment variables: GRPC_TRACE=http and GRPC_VERBOSITY=DEBUG to see all the plaintext of the HTTP traffic we're actually sending. You'll see the token we're passing base64-encoded in a header like authorization: Bearer xxxyyyzzz.

Alternatively, you could set a breakpoint in Stream::ResumeStartWithCredentials (here), and observe the state of the token at the point where we're starting up the Listen RPC stream.

@paulb777
Copy link
Member

Still digging into it, but definite differences in the logs with the GRPC environment variables turned on:

Emulator log

xuQfk95V9JZrWqgN7EsWsJB61ln1
D0414 11:14:36.531198000 123145440624640 ev_posix.cc:174]              Using polling engine: poll
D0414 11:14:36.531493000 123145440624640 dns_resolver.cc:289]          Using native dns resolver
2020-04-14 11:14:37.869362-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Firestore][I-FST000001] Initializing. Current user: xuQfk95V9JZrWqgN7EsWsJB61ln1
2020-04-14 11:14:37.869502-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Firestore][I-FST000001] Using full collection scan to execute query: Query(canonical_id=users/xuQfk95V9JZrWqgN7EsWsJB61ln1|f:|ob:__name__asc)
2020-04-14 11:14:37.869616-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Firestore][I-FST000001] WatchStream (7fa3a770f640) start
2020-04-14 11:14:37.869717-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Auth][I-AUT000002] Token auto-refresh enabled.
2020-04-14 11:14:37.869854-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Auth][I-AUT000004] Token auto-refresh scheduled in 53:21 for the new token.
2020-04-14 11:14:37.869999-0700 EmulatorSample[70980:14938689] 6.19.0 - [Firebase/Firestore][I-FST000001] Creating Firestore stub.
D0414 11:14:37.874074000 123145442770944 dns_resolver.cc:242]          Start resolving.
I0414 11:14:37.876910000 123145442234368 subchannel.cc:926]            Connect failed: {"created":"@1586888077.876847000","description":"Failed to connect to remote host: Connection refused","errno":61,"file":"/Users/paulbeusterien/bugs/5072/firestore-emulator-sample/iOS/Pods/gRPC-Core/src/core/lib/iomgr/tcp_client_posix.cc","file_line":207,"os_error":"Connection refused","syscall":"connect","target_address":"ipv6:[::1]:8080"}

Non-emulator log

xuQfk95V9JZrWqgN7EsWsJB61ln1
2020-04-14 11:20:12.246436-0700 EmulatorSample[71250:14944552] 6.19.0 - [Firebase/Firestore][I-FST000001] Initializing. Current user: xuQfk95V9JZrWqgN7EsWsJB61ln1
2020-04-14 11:20:12.247897-0700 EmulatorSample[71250:14944552] 6.19.0 - [Firebase/Firestore][I-FST000001] Using /Users/paulbeusterien/Library/Developer/CoreSimulator/Devices/5E080713-0767-4686-8376-06994CABEA16/data/Containers/Data/Application/D05E99B2-3B39-4D98-8F35-5AA3F8821CE1/Library/Application Support/firestore/__FIRAPP_DEFAULT/emulator-auth-sample/main for LevelDB storage
2020-04-14 11:20:12.303827-0700 EmulatorSample[71250:14944552] 6.19.0 - [Firebase/Firestore][I-FST000001] Committing transaction: <LevelDbTransaction Start LevelDB: 0 changes (0 bytes):>
D0414 11:20:12.305851000 123145519562752 ev_posix.cc:174]              Using polling engine: poll
D0414 11:20:12.306094000 123145519562752 dns_resolver.cc:289]          Using native dns resolver
2020-04-14 11:20:12.307349-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Committing transaction: <LevelDbTransaction Start MutationQueue: 0 changes (0 bytes):>
2020-04-14 11:20:12.307731-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Committing transaction: <LevelDbTransaction NextMutationBatchAfterBatchID: 0 changes (0 bytes):>
2020-04-14 11:20:12.309070-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Committing transaction: <LevelDbTransaction Allocate target: 0 changes (0 bytes):>
2020-04-14 11:20:12.309605-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Using full collection scan to execute query: Query(canonical_id=users/xuQfk95V9JZrWqgN7EsWsJB61ln1|f:|ob:__name__asc)
2020-04-14 11:20:12.310333-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Committing transaction: <LevelDbTransaction ExecuteQuery: 0 changes (0 bytes):>
2020-04-14 11:20:12.311255-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] WatchStream (7fe582d15f70) start
2020-04-14 11:20:15.202575-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Auth][I-AUT000002] Token auto-refresh enabled.
2020-04-14 11:20:15.202851-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Auth][I-AUT000004] Token auto-refresh scheduled in 47:45 for the new token.
2020-04-14 11:20:15.203029-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] Creating Firestore stub.
2020-04-14 11:20:15.205757-0700 EmulatorSample[71250:14944167] 6.19.0 - [Firebase/Firestore][I-FST000001] WatchStream (7fe582d15f70) watch: <ListenRequest 0x70000d13b408>: {
  database: "projects/emulator-auth-sample/databases/(default)"
  add_target {
    documents {
      documents: "projects/emulator-auth-sample/databases/(default)/documents/users/xuQfk95V9JZrWqgN7EsWsJB61ln1"
    }
    resume_token: "\n\t\010\362\270\303\216\323\346\350\002"
    target_id: 2
  }
}
D0414 11:20:15.205856000 123145521709056 dns_resolver.cc:242]          Start resolving.
I0414 11:20:15.434381000 123145520099328 chttp2_transport.cc:839]      W:0x7fe583849000 CLIENT [ipv4:172.217.0.42:443] state IDLE -> WRITING [TRANSPORT_FLOW_CONTROL]
I0414 11:20:15.434569000 123145520099328 chttp2_transport.cc:839]      W:0x7fe583849000 CLIENT [ipv4:172.217.0.42:443] state WRITING -> WRITING+MORE [INITIAL_WRITE]
I0414 11:20:15.434773000 123145520099328 subchannel.cc:982]            New connected subchannel at 0x6000037a9080 for subchannel 0x7fe582f33f50

@paulb777
Copy link
Member

@wilhuff The token looks correct at the suggested breakpoint. The "Connection refused" problem happens later in the bowels of gRPC

(lldb) p maybe_token
(const firebase::firestore::util::StatusOr<firebase::firestore::auth::Token>) $0 = {
  firebase::firestore::util::internal_statusor::StatusOrData<firebase::firestore::auth::Token> = {
     = {
      status_ = {
        state_ = {
          __ptr_ = {
            std::__1::__compressed_pair_elem<firebase::firestore::util::Status::State *, 0, false> = {
              __value_ = 0x0000000000000000
            }
          }
        }
      }
    }
     = {
      dummy_ = {}
      data_ = {
        token_ = "eyJhbGciOiJSUzI1NiIsImtpZCI6ImRjMGMzNWZlYjBjODIzYjQyNzdkZDBhYjIwNDQzMDY5ZGYzMGZkZWEiLCJ0eXAiOiJKV1QifQ.eyJwcm92aWRlcl9pZCI6ImFub255bW91cyIsImlzcyI6Imh0dHBzOi8vc2VjdXJldG9rZW4uZ29vZ2xlLmNvbS9lbXVsYXRvci1hdXRoLXNhbXBsZSIsImF1ZCI6ImVtdWxhdG9yLWF1dGgtc2FtcGxlIiwiYXV0aF90aW1lIjoxNTg2ODIxMzkyLCJ1c2VyX2lkIjoieHVRZms5NVY5SlpyV3FnTjdFc1dzSkI2MWxuMSIsInN1YiI6Inh1UWZrOTVWOUpacldxZ043RXNXc0pCNjFsbjEiLCJpYXQiOjE1ODY4ODc5NzcsImV4cCI6MTU4Njg5MTU3NywiZmlyZWJhc2UiOnsiaWRlbnRpdGllcyI6e30sInNpZ25faW5fcHJvdmlkZXIiOiJhbm9ueW1vdXMifX0.JBCUCpLPh4JnrDK73tOGicN2GnJdBNKJu4YjbJDd9eEexx8xz_M7isCAFcTiNgK0Pqjsq81Ld5r_4P6IiPUQoCV07fkALOAkyuzMwS3SRukC-rsmZbYblaHqVNcBRg24uWcexl0u6_3CAzTG9BFHXXusE7c0JPQfYGAeuVrzPQNVdxD_hKD0GyNwmHeCK25xmOpjyUUM2MBVDb7FNrlQDcL0ojDkBPBSlpWdm8r3lZiNKwIUsq0_4muVCKKQlM1JazOkeomGNQtI69XnJcTA1jVF5jpQaKy87ohD5EIRPWpa77SKQXtnH6nwUfrQgc-w51CiW3y8Q4YLZvG1hhjsCg"
        user_ = (uid_ = "xuQfk95V9JZrWqgN7EsWsJB61ln1", is_authenticated_ = true)
      }
    }
  }
}

@paulb777
Copy link
Member

The two versions diverge at the getsockopt call in tcp_client_posix.cc.

The success case returns 0 in so_error and the failure case returns 61(ECONNREFUSED).

@wilhuff
Copy link
Contributor

wilhuff commented Apr 14, 2020

@paulb777 are you actually running an emulator on your local host, port 8080 while trying to reproduce this? Getting a connection refused there seems wholly disconnected from the underlying issue here.

For a quick way to run the emulator just call scripts/run_firestore_emulator.sh from the root of the repo.

@paulb777
Copy link
Member

I was running with the emulator bundled in the bug report. With the emulator from the repo, the first call to getsockopt still fails with ECONNREFUSED, but there is a second call that succeeds before the Swift callback successfully returns without error.

Correct behavior seems to depend on using version 1.10.4 of the emulator (or something else in scripts/run_firestore_emulator.sh). It looks like when I ran with the bug report instructions, I was running 1.10.2. I then did npm i -g firebase-tools and got 1.11.2 which failed differently - never returning to the Swift callback and continued to log the "Failed to connect" error message.

@paulb777
Copy link
Member

Removing the Auth tag since it seems the issue is somewhere in the emulator or how Firestore interacts with it.

@paulb777
Copy link
Member

My comment above about the emulator version mattering can be disregarded. I was confused about how the emulator works. If I modify scripts/run_firestore_emulator.sh to pass the same rules file I get the same behavior with the different emulators. The test fails if the rules file is used.

@wilhuff
Copy link
Contributor

wilhuff commented Apr 15, 2020

I pulled the reproduction up under the debugger and I can see that Firestore is setting the credential but gRPC isn't sending it. While it's possible there's something I'm not seeing here, the next step is to upgrade gRPC and see if the issue persists. We're quite a ways behind.

@wilhuff wilhuff self-assigned this Apr 15, 2020
@paulb777
Copy link
Member

Still the same issue after commenting out the downversion lock in the podspec:

--- a/FirebaseFirestore.podspec
+++ b/FirebaseFirestore.podspec
-  s.dependency 'gRPC-C++', '0.0.9'
+  s.dependency 'gRPC-C++'#, '0.0.9'
 pod update
Update all pods
Updating local specs repositories
Analyzing dependencies
Downloading dependencies
Installing BoringSSL-GRPC 0.0.5 (was 0.0.3 and source changed to `https://cdn.cocoapods.org/` from `trunk`)
Installing FirebaseFirestore 1.12.0
Installing gRPC-C++ 1.25.0 (was 0.0.9 and source changed to `https://cdn.cocoapods.org/` from `trunk`)
Installing gRPC-Core 1.25.0 (was 1.21.0 and source changed to `https://cdn.cocoapods.org/` from `trunk`)

@wilhuff
Copy link
Contributor

wilhuff commented Apr 16, 2020

After some research, this is essentially the same issue as reported in grpc/grpc-node#543 (there may be a more canonical issue for this but the discussion there highlights the issue). The gRPC C core is discarding the credential because we're connecting the emulator over an unencrypted channel.

I'll cook up a workaround.

@samtstern
Copy link
Contributor

@wilhuff thanks for investigating! FWIW Android sends the credentials properly so may be able to look at what happens over there.

@wilhuff
Copy link
Contributor

wilhuff commented Apr 16, 2020

Android sets the authorization header directly rather than using the credential APIs. IIRC there's some complicated credential composition thing you have to do there and this just turned out easier because we were already setting headers for other reasons. The workaround here will be similar.

@wilhuff
Copy link
Contributor

wilhuff commented Apr 21, 2020

This fix will go out with the next release (in a few weeks--there's a release that's coming out imminently that was cut well before this fix landed).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants