Firebaseui-android: User Signed In With FirebaseUI Shows As Null With Firestore Security Rules

Created on 15 Oct 2018  路  9Comments  路  Source: firebase/FirebaseUI-Android

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

When signing in via Firebase UI the request.auth.uid** != null is always null in 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

I've created a corresponding StackOverflow.

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
            }
        }
    }

}

All 9 comments

@AdamSHurwitz thank you for the very detailed bug report!

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)

@samtstern thank you for the fast response!

I implemented your refactor on both the Sample app and my production app with success! Firebase Security rules and request.auth.uid is working as expected now.

One question regarding the auth state: Do you need to retrieve the specific app instance as well when creating a FirebaseAnalytics object the same way a specific app instance needs to be used for Auth?

For instance, instead of FirebaseAnalytics.getInstance(application), using FirebaseAnalytics.getInstance(FirebaseApp.getInstance("firebaseAppName").applicationContext)

I've updated the related StackOverflow post with your solution. May you up vote to help others with a similar issue find this solution.

Yes in all cases where you call getInstance() it will assume you want to use the default app (named `[DEFAULT]') unless you specify otherwise.

If you only need one FirebaseApp in your application, you probably want to avoid using a custom one at all. Instead just make sure you have the right json or xml file so that the default app can be initialized correctly. It will save you a lot of headaches like this one!

Closing the issue since there's no bug, but feel free to follow up if you have more questions.

I'm using 4 Firebase projects currently. However, I am refactoring in order to simplify everything into two projects, one for _staging_ and _prodution_ overall.

Currently there are two projects for a _staging_ and _production_ version of price analytics data and another set of two projects for _staging_ and _production_ versions of media content and user data.

@AdamSHurwitz do you ever use both in the same APK? Or could you just have a different json file for each of the two build types?

@samtstern, exactly, there is a .json file in the directory for _qa_ and _release_ build types in order to associate the app with the appropriate Firebase project for the user and media content data.

The pricing data does not have a .json file since it is read only and doesn't need to interact with the other Firebase services. To reduce complexity and confusion I'm consolidating this data into the first two Firebase projects.

OK cool, that makes sense.

By the way for an app like yours, you can avoid the JSON file if you want to:
https://medium.com/@samstern_58566/how-to-use-firebase-on-android-without-the-google-services-plugin-93ecc7dc6c4

Interesting concept. I've been thinking about how to secure important Strings such as the ones generated from the google-services.json file with your strategy above. I've commented in your Medium post replies so others interested may see.

For what it's worth there are no secrets in the google-services.json file, assuming that your Firebase resources have the appropriate security rules.

Was this page helpful?
0 / 5 - 0 ratings