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.
I've created a corresponding StackOverflow.
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();
}
}
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.")
}
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'
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
request.auth was not null in the Firestore Rules file.request.auth.uid == userId in the rule above comparing the request.auth.uid to the userId in the path route.request.auth.token.email == userEmail in a rule which fails.implementation 'com.google.firebase:firebase-auth:16.0.4implementation 'com.google.android.gms:play-services-auth:16.0.1'firebase-core:16.0.4, firebase-firestore:17.1.1, firebase-ui-firestore:4.2.0, firebase-ui-auth:4.1.0. 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.
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)
service cloud.firestore {
match /databases/{database}/documents {
match /testCollection/testDoc {
allow read: if request.auth.uid != null
}
}
}
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
}
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'
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
}
}
}
}
@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.