My Firebase web app requires administrator access, i.e., the UI should show a few things only for admins (an 'administrator' section). I came up with the below as a means to authorize the UI to display the admin section for valid admins only. My question is, good or bad? Is this a sound means of authorizing? ...so many ways to do this. This particular way requires me to configure admins in the security rules (vs in a node/tree in a db/firestore)
My idea is that if the .get() fails due to unauthorized access, I tell my app logic the user is not an admin, if the .get() succeeds my logic shows the 'admin' sections. Of course, the 'sections' are just HTML skeletons/empty elements populated by the database so even if the end user hacks the JS/logic, no real data will be there - only the empty 'admin section' framework.
function isAdmin(){
return new Promise(function(resolve, reject){
var docRef = firebase.firestore().collection("authorize").doc("admin");
docRef.get().then(function(result) {
if (result) {
resolve (true);
}
}).catch(function(error) {
resolve (false);
});
});
}
The firestore rule specifies the 'admins' by UID.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid == "9mB3UxxxxxxxxxxxxxxxxxxCk1";
}
}
}
You're storing the role of each user in the database, and then looking it up in the client to update its UI. This used to be the idiomatic way for a long time on realtime database, and it still works on Firestore.
The only thing I'd change is to have the rules also read from /authorize/admin, instead of hard-coding the UID in them. That way you only have the UID in one place, instead of having it in both the rules and the document.
But you may also want to consider an alternative: set a custom claim on your admin user, that you can then read in both the server-side security rules (to enforce authorized access) and the front-end (to optimize the UI).
To set a custom claim you use the Firebase Admin SDK. You can do this on a custom server, in Cloud Functions, but in your scenario it may be simpler to just run it from your development machine.
Detailed How To: Firebase has what's called Custom Claims for this functionality as detailed in their Control Access with Custom Claims and Security Rules. Basically, you stand up a separate node server, install the Firebase AdminSDK:
npm install firebase-admin --save
Generate/Download a Private Key from the Service Accounts tab in the Firebase Console and put that on your node server. Then simply create a bare bones node app to assign Custom Claims against each UID (user) that you wish. Something like below worked for me:
var admin = require('firebase-admin');
var serviceAccount = require("./the-key-you-generated-and-downloaded.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://xxxxxxxxxxxxxxxxxxxx.firebaseio.com"
});
admin.auth().setCustomUserClaims("whatever-uid-you-want-to-assign-claim-to", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
That's it. You can now verify if the custom claim is applied by logging out of your app (if you were previously logged in) and logging back in after you update your web app's .onAuthStateChanged method:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
firebase.auth().currentUser.getIdToken()
.then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin.
if (!!payload['admin']) {
//showAdminUI();
console.log("we ARE an admin");
}
else {
console.log("we ARE NOT an admin");
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});
Related
I am building an app with Firebase that requires private messaging between users.
What I need is to build single chats for 1-1 chats, storing messages in Firestore.
My idea: I guess the best way is to build a single collection for each chat, with the right security rules. Let's say an user with tokenID 1234 wants to talk with user with tokenID 1111, a new collection called 1234_1111 will be created (if not existing), and the security will allow only these two users to read and write.
My question: Is it the right way? And how to do that in Javascript? I'm not sure how to define security rules directly in the JS code, neither how to create a collection with the two users ID.
Security Rules are not defined in your JavaScript code, they are defined separately. What you suggest would be a reasonable approach to take although I'd use a subcollection for it, and a simplified version of your security rules might look something like:
service cloud.firestore {
match /databases/{database}/documents {
match /dms/{idPair}/messages/{msgId} {
allow read, write: if
idPair.split('_')[0] == request.auth.uid ||
idPair.split('_')[1] == request.auth.uid;
}
}
}
Then in your JS code you could do something along the lines of:
// generate idPair
function dmCollection(uid) {
const idPair = [firebase.auth().currentUser.uid, toUid].join('_').sort();
return firebase.firestore().collection('dms').doc(idPair).collection('messages');
}
// send a DM
function sendDM(toUid, messageText) {
return dmCollection(toUid).add({
from: firebase.auth().currentUser.uid,
text: messageText,
sent: firebase.firestore.FieldValue.serverTimestamp(),
});
}
// retrieve DMs
function messagesWith(uid) {
return dmCollection(uid).orderBy('sent', 'desc').get();
}
Note that the idPair is constructed by joining a sorted pair of UIDs so that it will be stable no matter which user sends.
I am creating a React web app where the user sign in/up and other authentication related processes are being handled by AWS Cognito and the accompanying Javascript SDK.
My app has some 'public' routes/pages that everybody, signed in or not, can view, such as /documentation/ and /sign-in/. There also exist various private routes which you can only see when you are logged in, such as /my-documents/.
At the moment, I have a working sign in page, where a user is signed in with code very similar to use case #4 (Cognito Docs).
My question now is: as soon as a user goes to /my-documents/, how do I check whether the user is signed in and actually has the rights to see this page?
I am not using AWS Amplify for the authentication in my app. I only use the NPM package 'amazon-cognito-identity-js'.
This is the code I currently use to check if the session is valid, in other words if the user is successfully signed in. This however, seems like a cumbersome way to check such a simple status.
const isAuthenticated = () => {
const cognitoUser = userPool.getCurrentUser();
let isSessionValid = false;
if (cognitoUser) {
cognitoUser.getSession((err: Error, result: CognitoUserSession) => {
if (!err) {
isSessionValid = result.isValid();
}
});
}
return isSessionValid;
};
isSessionValid is returned before the callback in getSession is executed.
I'm using the Firebase Real Time Database and I need to add user authentication to it. Users can only login with Google as a provider.
Current database mode:
{
"rules": {
".read": true,
".write": true
}
}
New mode should be like this:
// These rules grant access to a node matching the authenticated
// user's ID from the Firebase auth token
{
"rules": {
"users": {
"$uid": {
".read": "$uid === auth.uid",
".write": "$uid === auth.uid"
}
}
}
}
What should I use to authenticate in my case? userID, Google providerID or a token like described here?
This is the function without authentication to store data:
createMeetup ({commit, getters}, payload) {
console.log('index.js -> createMeetup')
const meetup = {
title: payload.title,
}
let imageUrl
let key
firebase.database().ref('meetups').push(meetup)
.then((data) => {
key = data.key
return key
})
.then(() => {
commit('createMeetup', {
...meetup,
imageUrl: imageUrl,
id: key
})
})
.catch((error) => {
console.log(error)
})
},
For your use case it seems like you need to sort out a few steps. I'm guessing your application can already connect/use Firebase, but these are essentially it:
Step 1 - Connecting
Connect to Firebase using your API key/config as per usual, should look something like the following.
firebase.initializeApp(config)
See also: https://firebase.google.com/docs/web/setup
You probably already have this somewhere. This does not change, but if you would apply the rules as described your users would not be able to use Firebase after just connecting.
Step 2 - Authenticating
This is basically telling Firebase who is connected. This must be done with a token/method Firebase can verify. Using a Google ID is the most common method.
With an existing Google ID / user login
// Initialize a generate OAuth provider with a `google.com` providerId.
var provider = new firebase.auth.OAuthProvider('google.com');
var credential = provider.credential(googleUser.getAuthResponse().id_token);
firebase.auth().signInWithCredential(credential)
See also: https://firebase.google.com/docs/reference/js/firebase.auth.OAuthProvider#credential
Or make Firebase SDK do the login flow
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
See also: https://firebase.google.com/docs/auth/web/google-signin
This last option is preferred / suggested by the documentation you referenced.
If, as you described, users can already login with Google to your app for other functionalities then you should already have a login flow somewhere. Depending on your situation it might be advisable to let the Firebase SDK / library take over this process for simplicity in your application.
Step 3 - Using the database
Lastly, after authenticating users and applying the rules you suggested you will need to also make sure the paths you write to are within those accessible by the current user. You can put this in a simple function to make it easier.
const getUserRef = (ref) => {
const user = firebase.auth().currentUser;
return firebase.database().ref(`/users/${user.uid}/${ref}/`);
}
You should of course not be retrieving the current user every time you want to get a database reference, but I think this clearly illustrates the steps that need to be taken.
You can allow users to login/auth using multiple methods. Then you can merge them together to a single account as described here:
https://firebase.google.com/docs/auth/web/account-linking
So really it boils down to two options:
Allow users to login with multiple methods such as Facebook, Google, Github, basic username/password, etc.
Or allow only a single login method such as Google only.
Whichever options you pick will help you decide which ID to use.
The auth rules in your question are only stating that the users can read/write their own (presumably) user data.
I assume that you are rather looking for a solution to authorize the user to create meetup data and you should crerate rules similar to this:
These rules allow any user that is logged in to create meetups
{
"rules": {
"meetups": {
"$meetupId": {
".read": "auth.uid != null",
".write": "auth.uid != null"
}
}
}
}
Your code-snippet that pushes new meetup data to the database will automatically try and succeed or fail depending on whether the user was logged in or not. You don't need to specifically tell Firebase in which way the user was logged in. Firebase SDK will take care of the authentication for you.
But if you do want to provide different mechanisms depending on which login type that the user is authenticated with, you can check it in the rules. For example if you want to make sure that the user is not just "anonymously" logged in.
See the documentation: https://firebase.google.com/docs/database/security/user-security#section-variable
the documentation has you covered there: Authenticate Using Google Sign-In with JavaScript.
You can let your users authenticate with Firebase using their Google Accounts by integrating Google Sign-In into your app. You can integrate Google Sign-In either by using the Firebase SDK to carry out the sign-in flow, or by carrying out the Google Sign-In flow manually and passing the resulting ID token to Firebase.
Before you begin:
Add Firebase to your JavaScript project.
Enable Google Sign-In in the Firebase console:
In the Firebase console, open the Auth section.
On the Sign in method tab, enable the Google sign-in method and click Save.
Handle the sign-in flow with the Firebase SDK
If you are building a web app, the easiest way to authenticate your users with
Firebase using their Google Accounts is to handle the sign-in flow with the
Firebase JavaScript SDK. (If you want to authenticate a user in Node.js or
other non-browser environment, you must handle the sign-in flow manually.)
To handle the sign-in flow with the Firebase JavaScript SDK, follow these steps:
Create an instance of the Google provider object:
var provider = new firebase.auth.GoogleAuthProvider();
Optional: Specify additional OAuth 2.0 scopes that you want to request from the authentication provider. To add a scope, call addScope().
For example:
provider.addScope('https://www.googleapis.com/auth/contacts.readonly');
See the authentication provider documentation.
Optional: To localize the provider's OAuth flow to the user's preferred language without explicitly passing the relevant custom OAuth parameters, update the language code on the Auth instance before starting the OAuth flow.
For example:
firebase.auth().languageCode = 'pt';
// To apply the default browser preference instead of explicitly setting it.
// firebase.auth().useDeviceLanguage();
Optional: Specify additional custom OAuth provider parameters that you want to send with the OAuth request. To add a custom parameter, call setCustomParameters on the initialized provider with an object containing the key as specified by the OAuth provider documentation and the corresponding value.
For example:
provider.setCustomParameters({
'login_hint': 'user#example.com'
});
Reserved required OAuth parameters are not allowed and will be ignored. See the authentication provider reference for more details.
Authenticate with Firebase using the Google provider object. You can prompt your users to sign in with their Google Accounts either by opening a pop-up window or by redirecting to the sign-in page. The redirect method is preferred on mobile devices.
To sign in with a pop-up window, call signInWithPopup:
firebase.auth().signInWithPopup(provider).then(function(result) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
Also notice that you can retrieve the Google provider's OAuth token which can be used to fetch additional data using the Google APIs.
This is also where you can catch and handle errors. For a list of error codes have a look at the Auth Reference Docs.
To sign in by redirecting to the sign-in page, call signInWithRedirect:
firebase.auth().signInWithRedirect(provider);
Then, you can also retrieve the Google provider's OAuth token by calling getRedirectResult() when your page loads:
firebase.auth().getRedirectResult().then(function(result) {
if (result.credential) {
// This gives you a Google Access Token. You can use it to access the Google API.
var token = result.credential.accessToken;
// ...
}
// The signed-in user info.
var user = result.user;
}).catch(function(error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
// The email of the user's account used.
var email = error.email;
// The firebase.auth.AuthCredential type that was used.
var credential = error.credential;
// ...
});
I was previously using firebase realtime database however now want to switch over to Cloud Firestore but am getting the below error, even when authenticated. I'm currently using Android Simulator, tried disabling my realtime database but cannot find a solution.
Firebase v5.4.2
[2018-09-02T12:53:42.064Z] #firebase/firestore:', 'Firestore (5.0.4):
Could not reach Cloud Firestore backend. Backend didn't respond within
10 seconds. This typically indicates that your device does not have a
healthy Internet connection at the moment. The client will operate in
offline mode until it is able to successfully connect to the backend.
My config is setup:
{
"apiKey": "apiKey",
"authDomain": "authDomain.firebaseapp.com",
"databaseURL": "https://databaseURL.firebaseio.com",
"projectId": "projectID",
"storageBucket": "storageBucket.appspot.com",
"messagingSenderId": "messagingSenderId"
}
As per docs I'm adding a users collection. Note that I do not get into the .then or the .catch statement even if I setup a users collection manually against the database.
onTestPress() {
console.log("onTestPress");
var db = firebase.firestore();
//console.log(db);
const settings = { timestampsInSnapshots: true };
db.settings(settings);
db.collection("users").add({
first: "Ada",
last: "Lovelace"
}).then(function (docRef) {
console.log("adding document: ", docRef.id);
}).catch(function (error) {
console.log("Error adding document: ", error);
});
db.collection("users").get().then((querySnapshot) => {
console.log(`querySnapshot: ${querySnapshot}`)
querySnapshot.forEach((doc) => {
console.log(`${doc.id} => ${JSON.stringify(doc.data())}`);
});
});
}
This is from Android Logcat you can see the .get() query returns the local copy. Only thing suspicious is the duplicate layer name?
09-02 14:05:37.811 2987-4016/com.myApp I/ReactNativeJS: onTestPress
09-02 14:05:47.837 2987-4016/com.myApp E/ReactNativeJS: '[2018-09-02T13:05:47.837Z] #firebase/firestore:', 'Firestore (5.0.4): Could not reach Cloud Firestore backend. Backend didn\'t respond within 10 seconds.\nThis typically indicates that your device does not have a healthy Internet connection at the moment. The client will operate in offline mode until it is able to successfully connect to the backend.'
09-02 14:05:47.849 2987-4016/com.myApp I/ReactNativeJS: querySnapshot: [object Object]
09-02 14:05:47.849 2987-4016/com.myApp I/ReactNativeJS: 3jifIc5kyEkGU4Bzvau9 => {"first":"Ada","last":"Lovelace"}
09-02 14:05:47.858 1431-1431/? D/SurfaceFlinger: duplicate layer name: changing com.myApp/com.myApp.MainActivity to com.myApp/com.myApp.MainActivity#1
09-02 14:05:47.931 2987-3021/com.myApp D/EGL_emulation: eglMakeCurrent: 0x9d7857e0: ver 3 0 (tinfo 0x9d783540)
Here's my import:
import firebase from "firebase";
import '#firebase/firestore'
Rules setup which should allow read and write?
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
I'm really not sure what else I can check so any advise would be greatly appreciated!
I also had this issue. The warning should appear after 10 seconds. but in my case, it appears when i load the component, without timeout. because my laptop's time in not correct. I did correct time and still had that issue. so i had to turn on "Set time automatically" and "Set time zone automatically" in "Date & time" settings in windows. everything works out for me.
The database structure looks like this
-LGw89Lx5CA9mOe1fSRQ {
uid: "FzobH6xDhHhtjbfqxlHR5nTobL62"
image: "https://pbs.twimg.com/profile_images/8950378298..."
location: "Lorem ipsum, lorem ipsum"
name: "Lorem ipsum"
provider: "twitter.com"
}
How can I delete everything, including the -LGw89Lx5CA9mOe1fSRQ key programmatically?
I looked at this, but it's outdated and deprecated Firebase: removeUser() but need to remove data stored under that uid
I've also looked at this, but this requires for user to constantly sign in (I'm saving the user ID in localStorage) and it returns null on refresh if I write firebase.auth().currentUser. Data records and user accounts are created through social network providers and I can see the data both on Authentication and Database tab in the Firebase console.
I've tried with these piece of code but it does nothing.
// currentUser has a value of UID from Firebase
// The value is stored in localStorage
databaseChild.child(currentUser).remove()
.then(res => {
// res returns 'undefined'
console.log('Deleted', res);
})
.catch(err => console.error(err));
The bottom line is, I need to delete the user (with a specific UID) from the Authentication tab and from the Database at the same time with one click.
I know that there is a Firebase Admin SDK but I'm creating a Single Page Application and I don't have any back end code. Everything is being done on the front end.
Any kind of help is appreciated.
With suggestions from #jeremyw and #peter-haddad I was able to get exactly what I want. Here is the code that is hosted on Firebase Cloud Functions
const functions = require('firebase-functions'),
admin = require('firebase-admin');
admin.initializeApp();
exports.deleteUser = functions.https.onRequest((request, response) => {
const data = JSON.parse(request.body),
user = data.uid;
// Delete user record from Authentication
admin.auth().deleteUser(user)
.then(() => {
console.log('User Authentication record deleted');
return;
})
.catch(() => console.error('Error while trying to delete the user', err));
// Delete user record from Real Time Database
admin.database().ref().child('people').orderByChild('uid').equalTo(user).once('value', snap => {
let userData = snap.val();
for (let key of Object.keys(userData)) {
admin.database().ref().child('people').child(key).remove();
}
});
response.send(200);
});
Also, if you are facing CORS errors, add the mode: 'no-cors' option to your fetch() function and it will work without any problems.
The link you already found for deleting the user-login-account client-side is your only option if you want to keep the action on the client. Usually you want to keep most of the actions for things like account creation/deletion on the server for security reasons, and Firebase forces the issue. You can only delete your account if you were recently logged in, you can't have client-side start deleting old/random accounts.
The better option is to create your own Cloud Function to handle everything related to deleting a user. You would have to use the Admin SDK that you already found for this... but you could have that Cloud Function perform as many actions as you want - it will have to delete the user from the Auth tab, and delete the matching data in the Database.