Firebase JS Error: getToken aborted due to token change - firebase

I'm getting a Firebase error "Error: getToken aborted due to token change" while running Firestore transaction using the JavaScript library. The error doesn't get thrown every time and I couldn't find the pattern. I suppose I've implemented some race conditions somewhere.
The user flow in my app goes like this:
Register a new account & submit an additional string in the same form
Log user in after registration using the same credentials
After log in, take that additional string and save it to Firestore (in a transaction).
Transaction fails due to Error: getToken aborted due to token change.
The flow of promises:
firebase.auth().createUserWithEmailAndPassword(email, password)
.catch(signupError => {
// no problems here
})
.then(() => {
return firebase.auth().signInWithEmailAndPassword(email, password)
})
.catch(loginError => {
// no problem here
})
.then((user) => {
// Database write call which fails (see lower code block)
return this.claimInput.writeClaimedPlace(user.user.uid, claimedPlace);
})
.catch(error => {
// "getToken aborted" ERROR GETS CAUGHT HERE, transaction fails
})
}
The database transaction call
firestore.runTransaction(function(transaction) {
return transaction.get(usersBusinessRef).then(function(usersBusinesDoc) {
let claimedPlaces = [];
if (usersBusinesDoc.exists && usersBusinesDoc.data().claimedPlaces) {
claimedPlaces = usersBusinesDoc.data().claimedPlaces;
}
claimedPlaces.push(claimedPlace);
return transaction.set(usersBusinessRef, { claimedPlaces }, { merge: true });
});
});
I couldn't find the error anywhere on google.
I'm thinking the error is caused by the token change that happens at log in. On the other hand I'm reading that Firebase accepts old tokens for a few more moments. Any thoughts?

I got a similar error[error code] while debugging my client which was connecting to firebase via a React App.
The solution was
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write: if false;
}
}
}
Putting the above inside the rules part of the firestore settings which apparently means you need to allow reads for external apis but writes are blocked and it was previously blocking both reads and writes.
This could be one of the issues if you are trying to read from your client/server
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write;
}
}
}
DISCLAIMER: * I am not an Expert at firebase. I am not sure if this would compromise your DB to be written by external apis since you are opening your firestore firewall by doing this
P.S. there is a firebase-admin module which I think helps in doing writes by handling authentication in a separate fashion. I think that module is more suited for writes and the normal firebase.firestore(app) is for reads.

Related

Firestore token doesn't update in security rules after setting custom claim

TLDR;
Setup firestore security rules based on custom claim.
Cloud firestore user is created by phone auth.
Cloud function triggers on create user and adds a custom claim role- admin. An entry is updated in realtime database to indicate the claim update.
Listen to updates to realtime database in the client and call user.getIdToken(true); after custom claim is updated.
Able to see the added custom claim in the code.
Unable to read a document in firestore due to missing permission(custom claim).
Refresh the browser page, able to read the doc now.
I have a cloud function that adds a custom claim role - admin on user create.
exports.processSignUp = functions.auth.user().onCreate((user) => {
const customClaims = {
role: 'admin',
};
// Set custom user claims on this newly created user.
return admin.auth().setCustomUserClaims(user.uid, customClaims)
.then(() => {
// Update real-time database to notify client to force refresh.
const metadataRef = admin.database().ref("metadata/" + user.uid);
// Set the refresh time to the current UTC timestamp.
// This will be captured on the client to force a token refresh.
return metadataRef.set({refreshTime: new Date().getTime()});
})
.catch(error => {
console.log(error);
});
});
I listen to change events in realtime database to detect updates to custom claim for a user.
let callback = null;
let metadataRef = null;
firebase.auth().onAuthStateChanged(user => {
// Remove previous listener.
if (callback) {
metadataRef.off('value', callback);
}
// On user login add new listener.
if (user) {
// Check if refresh is required.
metadataRef = firebase.database().ref('metadata/' + user.uid + '/refreshTime');
callback = (snapshot) => {
// Force refresh to pick up the latest custom claims changes.
// Note this is always triggered on first call. Further optimization could be
// added to avoid the initial trigger when the token is issued and already contains
// the latest claims.
user.getIdToken(true);
};
// Subscribe new listener to changes on that node.
metadataRef.on('value', callback);
}
});
I have the following security rules on cloud firestore.
service cloud.firestore {
match /databases/{database}/documents {
match /{role}/{document=**} {
allow read: if request.auth != null &&
request.auth.token.role == role;
}
}
}
After the user is created, my cloud function triggers and adds the custom claim role = admin.
As a result of user.getIdToken(true); the token is refreshed on my client and I am able to see the set custom claim.
When I try to get a document that the user should be able to read, I get a permission denied by cloud firestore security rules.
When I refresh the browser page, I am able to read the documents in the path.
I am expecting that to be able to access the firebase doc without having to refresh the browser. Is this a possibility?
Can someone please tell me what is wrong with my approach/expectation?
It can take longer than you think for the token to be propagated. I use custom claims extensively - what I have found to work is to setup .onIdTokenChanged() to track uid & token updates, and then explicitly call .getIdTokenResult(true) to update my local token. Only after both are complete can you make customClaim secured calls to Firestore and/or RTDB.

Firebase Storage security rules and download tokens for uploaded files

TLDR: This is a question about the URL returned from the task.snapshot.ref.getDownloadURL() and its respective downloadToken. It asks about what is the main role and functionality of the token and if it's necessary for files that will be publicly available by security rules.
I've just finished the tutorial guides about uploading and downloading files to Firebase Storage found on https://firebase.google.com/docs/storage/web/upload-files and also on this Youtube tutorial from the Firebase official channel.
I'm building a Content Management System for a blog section in one of my Firebase web apps (React + Firebase).
I have a component that lets the admin pick an image and upload it to Firebase Storage Bucket to be displayed in a specific blog post. All images to a specific blogPost should be inside a folder for the specific blog-post-slug.
Example:
//bucket/some-blog-post-slug/image1.jpg
Code that runs whenever the admin selected a new file on the <input type='file'/>:
function onFileSelect(e) {
const file = e.target.files[0];
const storageRef = firebase.storage().ref('some-slug/' + file.name);
const task = storageRef.put(file);
task.on('state_changed',
function progress(snapshot) {
setPercent((snapshot.bytesTransferred / snapshot.totalBytes) * 100);
},
function error(err) {
console.log(err);
},
function complete() {
console.log('Upload complete!');
task.snapshot.ref.getDownloadURL().then(function(downloadURL) {
console.log('File available at', downloadURL);
props.changeImageSrc(downloadURL);
});
}
);
}
The code above returns the downloadURL that will be saved to the Firestore inside the blogPost document.
The downloadURL has the following format:
https://firebasestorage.googleapis.com/v0/b/MYFIREBASEAPP.appspot.com/o/some-slug%2FILE_NAME.jpg?alt=media&token=TOKEN_VALUE
You can see that it comes with a "basic URL": https://firebasestorage.googleapis.com/v0/b/MYFIREBASEAPP.appspot.com/o/some-slug%2FILE_NAME.jpg
And the basicURL is appended with the following GET parameters:
alt=media and token=TOKEN_VALUE
I was not aware that I would be getting a token, so I'm now testing its behavior to know more about it.
BEHAVIOR WITH STORAGE READS ALLOWED:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}}}
When I access the basicURL:
I get an object in response with the uploaded file details:
{
"name": "some-slug/FILE_NAME.jpg",
"bucket": "MYBUCKET",
"generation": "GENERATION_NUMBER",
"metageneration": "1",
"contentType": "image/jpeg",
"timeCreated": "2019-06-05T13:53:57.070Z",
"updated": "2019-06-05T13:53:57.070Z",
"storageClass": "STANDARD",
"size": "815155",
"md5Hash": "Mj4aCPs21NUNxXpKg1bHirFIO0A==",
"contentEncoding": "identity",
"contentDisposition": "inline; filename*=utf-8''FILE_NAME.jpg",
"crc32c": "zhkQMQ==",
"etag": "CKu4a1+u2+0ucI412CEAE=",
"downloadTokens": "TOKEN_VALUE"
}
When I access the basicURL?alt=media
The image is displayed.
When I access the basicURL?alt=media&token=TOKEN_VALUE
The image is displayed.
BEHAVIOR WITH STORAGE READS RESTRICTED:
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}}}
When I access the basicURL:
I get the following error object:
{
"error": {
"code": 403,
"message": "Permission denied. Could not perform this operation"
}
}
When I access the basicURL?alt=media
I get the same error object:
{
"error": {
"code": 403,
"message": "Permission denied. Could not perform this operation"
}
}
When I access the basicURL?alt=media&token=TOKEN_VALUE
The image is displayed.
CONCLUSION AND QUESTIONS
It seems to me that the security rule allow read: if request.auth != null; should have blocked any reads from unauthorized users, but with the TOKEN parameter, the file is accessible even for requests without an auth object (Note: I wasn't logged in when I ran the tests above).
I know it's not the best practice to ask more than 1 question, but in this case I think it's necessary:
QUESTION 1:
What is this TOKEN mainly used for and why does it overried the auth rule?
QUESTION 2:
I want these images to be publicly availabe, as the blog section will be for all users. What URL should I save to Firestore?
Option 1: allow the reads for everyone and just save the basicURL.
Option 2: keep the reads restricted and save the basicURL + token.
QUESTION 3:
To display the images in <image src="imgSrc"> tags do I need the alt=media parameter? Or the basicURL ending in the FILE_NAME is good enough?
EDIT: QUESTION 3 ANSWER: Just tested it and found out that the alt=media GET parameter IS necessary to display the image inside an <img> tag.
NOTE: if you upload the same file and replace the old one, you'll get a different token each time and the older token becomes invalid.
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write;
}
}
}
This is security permissions for Firebase Storage. All Types of Data(Images, Video etc)
Unfortunately, described method from https://firebase.google.com/docs/storage/web/create-reference does not add authorization data to request and storage rule like
service firebase.storage {
match /b/{bucket}/o {
match /{allPaths=**} {
allow read, write: if request.auth != null;
}
}
}
returns permission denied,
using AngularFireStorage module resolves this issue.
import { AngularFireStorage } from '#angular/fire/storage';
constructor(
private storage: AngularFireStorage
) {
}
getFileUrl(path: string): Observable<string> {
const storageRef = this.storage.ref(path);
return from(storageRef.getDownloadURL());
}

Firestore database rule works in simulator, but not in app

I'm building out a React Native application and just starting to build out all of the security rules in my Firestore database. I have several rules that are working well, but there's one rule that's pretty much identical to the working ones that's giving me
Error: Missing or insufficient permissions.
What's even weirder is that when I test this rule in the Firestore simulator, it passes without any issues.
I've tried shoving all of the conditional logic from the functions into the rule itself, but that did nothing.
Here are the relevant rules:
service cloud.firestore {
match /databases/{database}/documents {
function getCurrentTeam() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.lastTeam;
}
function signedIntoTeam() {
// For context, all "like" documents have an associated teamId.
return request.auth.uid != null && getCurrentTeam() == resource.data.teamId;
}
match /likes/{like} {
allow read: if signedIntoTeam();
}
}
}
Here's the function in my React Native app that's erroring out:
const getLikes = posts =>
new Promise((resolve, reject) => {
const likedPosts = [];
if (!posts.length) return resolve(likedPosts);
posts.forEach(post => {
if (post.likeCount > 0) {
firebase
.firestore()
.collection('likes')
.where('postId', '==', post.id)
.where('userId', '==', firebase.auth().currentUser.uid)
.get()
.then(querySnapshot => {
const likedByUser = querySnapshot.size > 0;
likedPosts.push({ ...post, likedByUser });
if (likedPosts.length === posts.length) resolve(likedPosts);
})
.catch(err => reject(err));
} else {
likedPosts.push({ ...post, likedByUser: false });
if (likedPosts.length === posts.length) resolve(likedPosts);
}
});
});
I have very similar functions and rules that work fine elsewhere. I just can't seem to figure out why this one in particular is failing.
Your rule is attempting to filter documents from the likes collection based on the contents of the document's teamId field. If your query does not also filter using this same criteria, the rule will always reject the request. This is because security rules are not filters. Please read the linked documentation.
With the rule you have in place, the client is always going to have to filter on the teamId field. Right now, it is just filtering on postId and userId. You should add teamId to that, or change the rule to match the query. The query and the rule need to be in sync - the query must only request documents that is knows to be readable by the rule - it should not rely on the rule to make changes to the result set.

How to force logout firebase auth user from app remotely

I have a project which uses firebase auth with firebaseUI to authenticate users. I have enabled Google, Facebook and email providers. What I need is to remotely logout or disable some of the users.
I want the users to logout from the app on doing so. I tried disabling the user in the firebase console and also used the firebase admin SDK (https://firebase.google.com/docs/auth/admin/manage-sessions) to revoke the refresh tokens.
I waited for more than 2 days and still noticed that the user was logged in and could access the firestore data.
I have also gone through and tried
Firebase still retrieving authData after deletion
Can anyone point to what I am doing wrong ?
You also cannot remotely force a user to be signed out. Any sign out will have to happen from the device that the user is signed in on.
There is no way to revoke an access token once that is minted. This means that even if you disable the user's account, they may continue to have access for up to an hour.
If that is too long, the trick (as also mentioned in my answer to the question you linked) is to maintain a list of blocked users in your database (or elsewhere) and then check against that in your security rules (or other authorization layer).
For example in the realtime database, you could create a list of blocked user's UIDs:
banned_uids: {
"uid1": true
"uid2": true
}
And then check against that in your security rules with:
".read": "auth.uid !== null && !root.child('banned_uids').child(auth.uid).exists()"
You can send a message data with FCM to force to log out.
For example, if the users use android application.
Save the FCM token in a collection in firebase Realtime.
configure the Android client app, in the service. LINK You have to make when receive a message with especial string, force to log out.
make the trigger you need in cloud functions, to send the data LINK when you need the user log out.
SUCCESS!
As per your scenarios, i assume that you need to make user logout when user is disabled.
Use One global variable to store TokenNo (might be in shared preference or sqlite):
Add following code to your manifest:
<service android:name=".YourFirebaseMessagingService">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
Add following code in your
public class LogoutOntokenchange extends FirebaseMessagingService{
#Override
public void onNewToken (String token){
if(TokenNo=>1){ //if tokenNo >=1 means he already logged in
TokenNo=0;
FirebaseAuth.getInstance().signOut(); //Then call signout method
}
else{
TokenNo=1; //store token no in db
}
}
}
What Happens here:
When user logged in first time onNewToken is called then It goes into else then TokenNo is updated to 1 from 0.
When You disable any user then automatically token is refreshed.Then OnNewToken is called then TokenNo>=1 so user will be logged out.
NOTE: When user log in for first time i.e if TokenNo variable is not stored then store it as 0.
For reference: https://firebase.google.com/docs/reference/android/com/google/firebase/messaging/FirebaseMessagingService
The only way I can think about is adding a if-else block in your starting activity.
Store the that status of user (verified/banned/deleted) in Firebase Real-time database. Then retrieve the status of user at start of application and add the code:
if (currentUserStatus.equals("banned"))
{
currentUser.logout();
}
What I've done is I created for each user upon registration a Firestore document with the UID as document ID. In this document I store an array which stores all fcm tokens the individual user receives when logging into a new device. That way I always keep track where the user is logged in. When the user logs out manually the fcm token will be deleted from the document in Firestore as well as on the device.
In order to be able to log out the user everywhere they are signed in I did the following. When starting the app and once the user is logged in I start a snapshot listener that listens to all changes in the users document. As soon as there is a change I retrieve the new array of fcm tokens, search inside the array for the local current device fcm token. If found, I do nothing. If the fcm token is no longer in the array I will call the local logout method and go back to the login screen.
Here are the methods I used in swift on iOS. The closures (passOnMethod) will just trigger an unwind segue to the login view controller.
import Foundation
import Firebase
class FB_Auth_Methods {
let db = Firestore.firestore()
var listener: ListenerRegistration?
func trackLoginStatus(passOnMethod: #escaping () -> () ) {
listener?.remove()
if let loggedInUserA_UID = Auth.auth().currentUser?.uid {
listener = db.collection(K.FStore.collectionOf_RegisteredUsers_Name)
.document(loggedInUserA_UID)
.addSnapshotListener { (snapshotDocument, error) in
if let error = error {
print(error)
} else {
if let document = snapshotDocument {
if let data = document.data() {
if let fcmTokens = data[K.FStore.Users.fcmTokens] as? [String] {
print("Found the following tokens: \(fcmTokens)")
self.compareTokensAgainstCurrentDeviceToken(fcmTokens: fcmTokens, passOnMethod: { () in
passOnMethod()
})
}
}
}
}
}
}
}
func compareTokensAgainstCurrentDeviceToken(fcmTokens: [String], passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
if fcmTokens.contains(result.token) {
print("Token found, doing nothing")
} else {
print("Token no longer found, logout user")
do {
try Auth.auth().signOut()
InstanceID.instanceID().deleteID { error in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
} catch let signOutError as NSError {
print (signOutError)
}
}
}
}
}
}
And here is the method I use when logging out the user everywhere but at the current device.
func deleteAllFcmTokensExceptCurrent(loggedInUserA: User, passOnMethod: #escaping () -> () ) {
InstanceID.instanceID().instanceID { (result, error) in
if let error = error {
print(error)
} else if let result = result {
let batch = self.db.batch()
let deleteAllFcmRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.delete()], forDocument: deleteAllFcmRef)
let updateFcmTokenRef = self.db.collection(K.FStore.collectionOf_RegisteredUsers_Name).document(loggedInUserA.uid)
batch.updateData([K.FStore.Users.fcmTokens: FieldValue.arrayUnion([result.token])], forDocument: updateFcmTokenRef)
batch.commit { (error) in
if let error = error {
print(error)
} else {
passOnMethod()
}
}
}
}
}
Not tested yet, as our backend programmer, who is in charge of setting up Firestore rules was gone for the day, but in theory this should work: (and it's something I'll test tomorrow)
Having a FirebaseAuth.AuthStateListener in charge of serving UI based on the status of the user
This combined with rules in firestore
match /collection
allow read: if isAuth();
Where isAuth is:
function isAuth() {
return request.auth.uid != null;
}
If the user is then disabled, while being logged in, whenever the user tries to read data from the collection, he should be denied, and a signOut() call should be made.
The AuthStateListener will then detect it, and sign the user out.

How to ensure Anonymous login with Firebase?

I'm working on a React Native application which use Firebase Auth uid to identify individual user instances.
My initial implementation called signInAnonymously on every starting up and it returned non-persistent uid.
Referring to Anonymous user in Firebase, my code became like this. it works as expected but still unclear why this code is correct.
static ensureLogin() {
return new Promise((resolve, reject) => {
firebase.auth().onAuthStateChanged((user) => {
if (user) {
resolve(user)
} else {
firebase.auth().signInAnonymously()
.catch(function(error) {
reject()
})
}
})
}
To answer this question, let's check details in this official document.
https://firebase.google.com/docs/auth/web/manage-users
It has this example code:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
The latter comment line says "No user is signed in." instead of "User is signed out.".
It implies that it is triggered not only by user's sign in/out actions.
It also says below.
By using an observer, you ensure that the Auth object isn't in an
intermediate state—such as initialization—when you get the current
user.
This means that 2 facts.
Auth object has internal states
onAuthStateChanged observes the state of Auth object
So, we should notice that onAuthStateChanged can be triggered by any status change of Auth object including initialization! And calling signInAnonymously after init is nothing strange.
Once signInAnonymously in else block is called, it re-triggers onAuthStateChanged after signing in.
That's why the code is correct.

Resources