2
votes

Environment

  • Android device: Google Pixel 2
  • Android OS version: 9
  • Google Play Services version: 14.3.66 (100400-213742215)
  • Firebase/Play Services SDK version: 16.0.1
  • FirebaseUI version: ui-firestore:4.2.0, ui-auth:4.1.0.

Issue

request.auth.uid != null is always null when using Firestore security rules.

When the uid is null the user on the Android client is both logged in via the Firebase UI and showing in the Firestore Authentication console.

enter image description here

Implementation

Sample Firestore Security Rule

service cloud.firestore {
function signedInOrPublic() {
  return request.auth.uid != null;
}

match /databases/{database}/documents {
  match /{document=**} {
    allow read;
  }
}

// Add to user collection.
match /databases/{database}/documents {
  match /users/{userId}/{collectionOrAction}/{content} {
    allow create: if signedInOrPublic();
  } 
}

Android Call to Firestore Collection

The call works as expected without any stringent rules set.

usersCollection.document(userId).collection(actionCollection)
.document(content.id)
.set(ContentAction(Date(), content.id, content.title, content.creator,
content.qualityScore)).addOnSuccessListener {
updateUserActionCounter(userId, countType)
}.addOnFailureListener {
Log.w(LOG_TAG, "User content action update FAIL.")
}

Firebase Libraries

implementation 'com.firebase:firebase-client-android:2.5.2'
implementation 'com.google.firebase:firebase-core:16.0.3'
implementation 'com.google.firebase:firebase-firestore:17.1.0'
implementation 'com.firebaseui:firebase-ui-firestore:4.1.0'
implementation 'com.firebaseui:firebase-ui-auth:4.0.0'

google-services.json Configuration

I have a version of the google-services.json for my debug and release builds in order to keep a separate Firestore project / environment / analytics for the production version of the app. I'm wondering if this could be effecting Firestore rules even though this setup is working as expected for every other part of Firebase so far (Firestore db, Analytics, etc.).

app>src>debug>google-services.json

app>src>release>google-services.json

Attempted Solutions

  1. I tried checking if request.auth was not null in the Firestore Rules file.
  2. I attempted to use request.auth.uid == userId in the rule above comparing the request.auth.uid to the userId in the path route.
  3. I attempted to use request.auth.token.email == userEmail in a rule which fails.
  4. Adding additional Firebase Auth library even though I already have the Firebase UI Auth library which is already as seen above: implementation 'com.google.firebase:firebase-auth:16.0.4
  5. Adding implementation 'com.google.android.gms:play-services-auth:16.0.1'
  6. Upgrading Firestore libraries to firebase-core:16.0.4, firebase-firestore:17.1.1, firebase-ui-firestore:4.2.0, firebase-ui-auth:4.1.0.
  7. I built a simple sample app in order to see if the security rules worked with a straightforward implementation.

Sample App

In the sample app there is a Sign in /out button which signs the user in and out depending on their current sign in state. The sign in state is displayed in the top left corner. The Get data button retrieves a String field from Firestore database based on the Firestore Security Rules.

enter image description here

Error

2018-10-14 15:21:59.123 24192-24192/firebase_security_sample.adamhurwitz.firebasesecuritysample         
E/AndroidRuntime: FATAL EXCEPTION: main
Process: firebase_security_sample.adamhurwitz.firebasesecuritysample, PID: 24192
com.google.android.gms.tasks.RuntimeExecutionException: com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
    at com.google.android.gms.tasks.zzu.getResult(Unknown Source:15)
    at firebase_security_sample.adamhurwitz.firebasesecuritysample.MainActivity$onCreate$2$1.onComplete(MainActivity.kt:63)
    at com.google.android.gms.tasks.zzj.run(Unknown Source:4)
    at android.os.Handler.handleCallback(Handler.java:873)
    at android.os.Handler.dispatchMessage(Handler.java:99)
    at android.os.Looper.loop(Looper.java:193)
    at android.app.ActivityThread.main(ActivityThread.java:6669)
    at java.lang.reflect.Method.invoke(Native Method)
    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
 Caused by: com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
    at com.google.firebase.firestore.obfuscated.zzhc.zza(com.google.firebase:firebase-firestore@@17.1.1:119)
    at com.google.firebase.firestore.obfuscated.zzk.zza(com.google.firebase:firebase-firestore@@17.1.1:134)
    at com.google.firebase.firestore.obfuscated.zzak.zza(com.google.firebase:firebase-firestore@@17.1.1:384)
    at com.google.firebase.firestore.obfuscated.zzm.zza(com.google.firebase:firebase-firestore@@17.1.1:235)
    at com.google.firebase.firestore.obfuscated.zzfv.zza(com.google.firebase:firebase-firestore@@17.1.1:7521)
    at com.google.firebase.firestore.obfuscated.zzfv$1.zza(com.google.firebase:firebase-firestore@@17.1.1:172)
    at com.google.firebase.firestore.obfuscated.zzgc.zzb(com.google.firebase:firebase-firestore@@17.1.1:3109)
    at com.google.firebase.firestore.obfuscated.zzfh.run(com.google.firebase:firebase-firestore@@17.1.1:1117)
    at com.google.firebase.firestore.obfuscated.zzfc$zza.zza(com.google.firebase:firebase-firestore@@17.1.1:67)
    at com.google.firebase.firestore.obfuscated.zzfc$zzc.zza(com.google.firebase:firebase-firestore@@17.1.1:110)
    at com.google.firebase.firestore.obfuscated.zzgv$1.onMessage(com.google.firebase:firebase-firestore@@17.1.1:107)
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:33)
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:33)
    at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1MessagesAvailable.runInContext(ClientCallImpl.java:526)
    at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37)
    at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
    at com.google.firebase.firestore.obfuscated.zzgf$zza.run(com.google.firebase:firebase-firestore@@17.1.1:203)
    at java.lang.Thread.run(Thread.java:764)
 Caused by: io.grpc.StatusException: PERMISSION_DENIED: Missing or insufficient permissions.
    at io.grpc.Status.asException(Status.java:534)
    at com.google.firebase.firestore.obfuscated.zzhc.zza(com.google.firebase:firebase-firestore@@17.1.1:117)
    at com.google.firebase.firestore.obfuscated.zzk.zza(com.google.firebase:firebase-firestore@@17.1.1:134) 
    at com.google.firebase.firestore.obfuscated.zzak.zza(com.google.firebase:firebase-firestore@@17.1.1:384) 
    at com.google.firebase.firestore.obfuscated.zzm.zza(com.google.firebase:firebase-firestore@@17.1.1:235) 
    at com.google.firebase.firestore.obfuscated.zzfv.zza(com.google.firebase:firebase-firestore@@17.1.1:7521) 
    at com.google.firebase.firestore.obfuscated.zzfv$1.zza(com.google.firebase:firebase-firestore@@17.1.1:172) 
    at com.google.firebase.firestore.obfuscated.zzgc.zzb(com.google.firebase:firebase-firestore@@17.1.1:3109) 
    at com.google.firebase.firestore.obfuscated.zzfh.run(com.google.firebase:firebase-firestore@@17.1.1:1117) 
    at com.google.firebase.firestore.obfuscated.zzfc$zza.zza(com.google.firebase:firebase-firestore@@17.1.1:67) 
    at com.google.firebase.firestore.obfuscated.zzfc$zzc.zza(com.google.firebase:firebase-firestore@@17.1.1:110) 
    at com.google.firebase.firestore.obfuscated.zzgv$1.onMessage(com.google.firebase:firebase-firestore@@17.1.1:107) 
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:33) 
    at io.grpc.ForwardingClientCallListener.onMessage(ForwardingClientCallListener.java:33) 
    at io.grpc.internal.ClientCallImpl$ClientStreamListenerImpl$1MessagesAvailable.runInContext(ClientCallImpl.java:526) 
    at io.grpc.internal.ContextRunnable.run(ContextRunnable.java:37) 
    at io.grpc.internal.SerializingExecutor.run(SerializingExecutor.java:123) 
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:458) 
    at java.util.concurrent.FutureTask.run(FutureTask.java:266) 
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:301) 
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167) 
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641) 
    at com.google.firebase.firestore.obfuscated.zzgf$zza.run(com.google.firebase:firebase-firestore@@17.1.1:203) 
    at java.lang.Thread.run(Thread.java:764) 

Firebase Security Rule

service cloud.firestore {

    match /databases/{database}/documents {
        match /testCollection/testDoc {
            allow read: if request.auth.uid != null
        }
    }
}

build.gradle (app)

buildscript {
ext.kotlin_version = '1.2.71'
repositories {
    google()
    jcenter()
}
dependencies {
    classpath 'com.android.tools.build:gradle:3.2.1'
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    classpath 'com.google.gms:google-services:4.1.0'

    // NOTE: Do not place your application dependencies here; they belong
    // in the individual module build.gradle files
}
}

allprojects {
repositories {
    google()
    jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

build.gradle (project)

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
       applicationId "firebase_security_sample.adamhurwitz.firebasesecuritysample"
    minSdkVersion 21
    targetSdkVersion 28
    versionCode 1
    versionName "1.0"
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'

    implementation 'com.google.android.gms:play-services-auth:16.0.1'
    implementation 'com.google.firebase:firebase-core:16.0.4'
    implementation 'com.google.firebase:firebase-auth:16.0.4'
    implementation 'com.google.firebase:firebase-firestore:17.1.1'
    implementation 'com.firebaseui:firebase-ui-firestore:4.2.0'
    implementation 'com.firebaseui:firebase-ui-auth:4.1.0'

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

apply plugin: 'com.google.gms.google-services'

MainActivity

class MainActivity : AppCompatActivity() {

lateinit var signInStatus: TextView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    FirebaseApp.initializeApp(
            this,
            FirebaseOptions.Builder()
                    .setApplicationId("1:63924060965:android:c387085babd1a8a4") // Required for Analytics.
                    .setApiKey("AIzaSyCi4h6WBX495xmzaRsLYro2_Vd9UcB3bpg") // Required for Auth.
                    .setDatabaseUrl("https://security-rules-sample.firebaseio.com") // Required for RTDB.
                    .setProjectId("security-rules-sample")
                    .build(),
            "firestoreSecuritySample")
    signInStatus = findViewById(R.id.signInStatus) as TextView

    val user = FirebaseAuth.getInstance().currentUser
    if (user != null) {
        signInStatus.text = user.displayName
    } else {
        signInStatus.text = "logged out"
    }

    signInButton.setOnClickListener {
        if (FirebaseAuth.getInstance().currentUser == null) {
            val providers = Arrays.asList<AuthUI.IdpConfig>(
                    AuthUI.IdpConfig.GoogleBuilder().build())

            startActivityForResult(
                    AuthUI.getInstance()
                            .createSignInIntentBuilder()
                            .setAvailableProviders(providers)
                            .build(),
                    123)
        } else {
            AuthUI.getInstance().signOut(this).addOnCompleteListener {
                signInStatus.text = "logged out"
            }
        }
    }

    getDataButton.setOnClickListener {
        FirebaseFirestore.getInstance(FirebaseApp.getInstance("firestoreSecuritySample"))
                .collection("testCollection").document("testDoc").get().addOnCompleteListener {
                    if (FirebaseAuth.getInstance().currentUser != null) {
                        firestoreResult.text = it.result!!.get("testField").toString()
                    }
                }
    }
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    if (requestCode == 123) {
        if (resultCode == Activity.RESULT_OK) {
            val user = FirebaseAuth.getInstance().currentUser
            if (user != null) {
                signInStatus.text = user.displayName
            }
        }
    }

}
1

1 Answers

2
votes

Thank you to Sam Stern from the Google Developer Relations team for helping debug! I've tested the fix below for both the sample app I created above as well as my app in production.

I am pretty sure your issue comes from this line:

FirebaseApp.getInstance("firestoreSecuritySample") You are using a custom FirebaseApp to talk to the database, but you're not using the same FirebaseApp when you sign in. There's a separate auth state for each app instance.

Try changing your sign in code to pass the app to getInstance:

        startActivityForResult(
                // SEE BELOW FOR CHANGE
                > AuthUI.getInstance(FirebaseApp.getInstance("firestoreSecuritySample"))
                        .createSignInIntentBuilder()
                        .setAvailableProviders(providers)
                        .build(),
                123)