Firebase security rules for nodejs app - node.js

I am new to firebase. I have below given code -
var config = {
apiKey: ".....",
authDomain: "....",
databaseURL: ".....",
storageBucket: "...."
};
firebase.initializeApp(config);
function writeData(socialType, userName, name, message) {
database.ref("/" + socialType + "/" + userName + "/").set({
name: name,
message: message
});
}
writeData("fb", "Jhon", "booo1", "message1");
I wonder what should be the security rules for these kind(i mean access with api key, and there is no login. In fact on firebase nodejs library firebase.auth() the auth method doesn't exist). I don't want to allow anonymous access, I want to let access if there is a valid api key.
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
For this security rules I get permission_denied error. Which probably makes sense, because there is no auth since I am not authenticating, but accessing through api key.
NOTE: I don't want to use service account. I want to use api key to access (read/write) data

The API key just identifies your project to the Firebase servers. It does not authenticate your node.js process with Firebase, nor does it grant you permission to access any data in the database.
To authenticate from your node.js process with Firebase, you will have to follow one of the methods outlined in the documentation. All of these require the use of a service account in some way, either to directly access the database or to sign a custom token. This is simply the way Firebase Authentication works for server-side processes and there is no way to avoid it.

The current issue with your rules is the:
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}
Since it appears that you don't have a login. If the browser code has the proper api keys you can use the firebase anonymous auth to allow auth !== null to succeed.
Also in the future I would suggest using the !== over the != to improve reliabilty

You have to manually call the signInWithEmail method on each endpoint before you access a firestore collection. Its annoying but no workaround
return await firebase.auth().signInWithEmailAndPassword("###", "###").then(async () => {
const user = await firebase.firestore().collection('user_info').doc('123').get()
return user.data();
})

Related

Google Sign-In idToken with createSessionCookie causing error - there is no user record corresponding to the provided identifier

Stack:
Google Sign-in (Vanilla JS - client side),
Firebase Functions (ExpressJS)
Client-Side:
My Firebase function express app uses vanilla javascript on the client side. To authenticate I am making use of Firebase's Google SignIn feature client-side javascript web apps, found here.
// Firebase setup
var firebaseConfig = {
apiKey: "AIza...",
authDomain: "....firebaseapp.com",
databaseURL: "https://...-default-rtdb.firebaseio.com",
...
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.NONE);
function postIdTokenToSessionLogin(idToken, csrfToken) {
return axios({
url: "/user/sessionLogin", < ----- endpoint code portion found below
method: "POST",
data: {
idToken: idToken,
csrfToken: csrfToken,
},
});
}
// ...
// On sign-in click
var provider = new firebase.auth.GoogleAuthProvider();
firebase.auth()
.signInWithPopup(provider)
.then(async value => {
const idToken = value.credential.idToken;
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
}).then(value => {
window.location.assign("/user/dashboard")
}).catch((error) => {
alert(error.message);
});
Note I am using value.credential.idToken (most sources imply to use this, but haven't found an example saying use this specifically)
Directly after calling signInWithPopup, a new account is created in my Firebase Console Authentication matching the gmail account that was just signed in.
Server-side:
Once I authenticate, I create an axios request passing in the {user}.credential.idToken and following the server-side setup here (ignoring the CSRF - this just doesn't want to work).
In creating the session, I use the following code in my firebase functions express app, the endpoint which is router.post('/sessionLogin', (req, res) => (part of /user route prefix):
// Set session expiration to 5 days.
const expiresIn = 60 * 60 * 24 * 5 * 1000;
const idToken = req.body.idToken.toString(); // eyJhbGciOiJSUzI1NiIsImt...[936]
admin
.auth()
.createSessionCookie(idToken, {expiresIn}) < ----------- Problem line
.then((sessionCookie) => {
// Set cookie policy for session cookie.
const options = {maxAge: expiresIn, httpOnly: true, secure: true};
res.cookie('session', sessionCookie, options);
res.end(JSON.stringify({status: 'success'}));
}).catch((error) => {
console.error(error);
res.status(401).send('UNAUTHORIZED REQUEST!');
});
On the createSessionCookie call, I get the following error & stack trace:
Error: There is no user record corresponding to the provided identifier.
at FirebaseAuthError.FirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:44:28)
at FirebaseAuthError.PrefixedFirebaseError [as constructor] (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:90:28)
at new FirebaseAuthError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:149:16)
at Function.FirebaseAuthError.fromServerError (C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\utils\error.js:188:16)
at C:\Users\CybeX\Bootstrap Studio Projects\future-design\functions\node_modules\firebase-admin\lib\auth\auth-api-request.js:1570:49
at processTicksAndRejections (internal/process/task_queues.js:93:5)
This is part of the sign-in flow with a existing Gmail account.
What is causing this?
After many hours of searching, Googling - I have seen the light.
For some additional context, this error featured heavily in my struggle "Firebase ID token has invalid signature." - I will get to that in a second.
Further, another issue I also faced was using a local auth emulator for web client-side (javascript), see this for setup.
TL;DR to solve the immediate problem
Client-side remained largely the same, however the documentation provided by Firebase was inaccurate/misleading - thanks to this post, I found the solution. Thus, it follows...
Which is the ID Token? (Client-side):
The examples from here (to allow signInWithPopup), the response (if successful) results in
...
.signInWithPopup(provider)
.then((result) => {
/** #type {firebase.auth.OAuthCredential} */
var credential = result.credential;
// This gives you a Google Access Token. You can use it to access the Google API.
var token = credential.accessToken;
// The signed-in user info.
var user = result.user;
// ...
})
Looking for an idToken, I found one using result.credential.idToken but no where on the internet on if this was infact the correct token to use.
I ran into this error using the provided idToken above:
Firebase ID token has incorrect "aud" (audience) claim. Expected
"[insert your **projectId**]" but got
"59895519979-2l78aklb7cdqlth0eob751mdm67kt301.apps.googleusercontent.com".
Make sure the ID token comes from the same Firebase project as the
service account used to authenticate this SDK.
Trying other tokens like result.credential.accessToken responded with various verification errors - what to do?
Mention earlier, this solution on Github suggested to use firebase.auth().currentUser.getIdToken() AFTER you have signed in. An example (building on my previous code) is to do the following:
...
.signInWithPopup(provider)
.then((result) => {
// current user is now valid and not null
firebase.auth().currentUser.getIdToken().then(idToken => {
// send this ID token to your server
const csrfToken = getCookie('_csrf');
return postIdTokenToSessionLogin(idToken, csrfToken);
})
})
At this point, you can verify your token and createSessionCookies to your heart's desire.
BUT, a secondary issue I unknowingly created for myself using the Authentication Emulator.
To setup for client-side use:
var auth = firebase.auth();
auth.useEmulator("http://localhost:9099");
To setup for hosting your firebase functions app (assuming you are using this with e.g. nodejs + express, see this for setup, ask in comments, can provide more details if needed)
Using Authentication Emulator caused the following errors AFTER using the above mentioned "fix". Thus, DO NOT RUN the local authentication emulator (with Google sign-in of a valid Google account) as you will consistently get.
Firebase ID token has invalid signature. See
https://firebase.google.com/docs/auth/admin/verify-id-tokens for
details on how to retrieve an ID token
You can use all your local emulators, but (so far in my experience) you will need to use an online authenticator.

Firebase Realtime Database Rules Denying Permission

I have an application I am building in Kotlin, with a backend in Node.js.
I am allowing users to login/signup using Firebase Authentication on the client application.
Part of the functionality of the application, is to let users save data online through Firebase's real time database. What happens inside the application, is once a user is logged in, I am passing his/her uid to the backend, which makes a request to the database.
Everything was working fine when the rules for the database were to allow read/write to everyone.
Once I changed them to this:
{
"rules": {
"users": {
".read": "auth != null && auth.uid != null",
".write": "auth != null && auth.uid != null",
}
}
}
I keep getting Permission Denied.
I have tried different variations of the rules:
Without the users key
Only checking that auth is not null
but none seem to work.
Is there some step I am missing?
I have combed over many similar StackOverflow questions and Firebase's real time database documentation, but have not found an answer to my problem.
Some code for reference:
Backend:
app.get('/someRoute', function (req, res) {
var database = firebase.database()
var uid = req.query.uid
database.ref('/users/' + uid).once('value')
.then(function(snapshot) {
var data = snapshot.val() ? snapshot.val() : []
res.status(200).send({ response: data})
}).catch(function(error) {
console.log(error)
res.status(500).json({ error: error})
})
})
Client:
fun loginUser(view : View) {
FirebaseAuth.getInstance().signInWithEmailAndPassword(userEmail, userPassword)
.addOnCompleteListener(this) { task ->
if (task.isSuccessful) {
updateFirebaseUserDisplayName()
} else {
Toast.makeText(this, "An error has occurred during login. Please try again later.", Toast.LENGTH_SHORT).show()
}
}
}
fun updateFirebaseUserDisplayName() {
FirebaseAuth.getInstance().currentUser?.apply {
val profileUpdates : UserProfileChangeRequest = UserProfileChangeRequest.Builder().setDisplayName(userEmail).build()
updateProfile(profileUpdates)?.addOnCompleteListener(OnCompleteListener {
when(it.isSuccessful) {
true -> apply {
Intent(this#LoginActivity, MainActivity::class.java).apply {
startActivity(this)
finish()
}
}
false -> Toast.makeText(this#LoginActivity, "Login has failed", Toast.LENGTH_SHORT).show()
}
})
}
}
After some searching, I found the solution to my problem.
It appears that because I was authenticating users on the client and having a backend that communicated with Firebase's Realtime Database, I had to use Firebase's Admin SDK in the backend.
This is because it was required to pass a unique token generated each time a user logs in and authenticates in the client. This token is then required to be sent to the backend and used when trying to access the Realtime Database.
For anyone else that will stumble upon this question and want to know how it can be done, follow the links below:
Adding Firebase Admin SDK
Verifying ID Tokens
Medium Article Explaining Everything
Also, make sure to reference your database name correctly
It seems a problem with the permission you have given to your node i.e users and try below way
Only authenticated users can access/write data
{
"rules": {
".read": "auth != null",
".write": "auth != null"
}
}

Firebase database access denied with permission set

I have carefully looked at other answers to similar questions and as far as I can tell everything is set correctly but still access is denied.
Minimum working example:
My Firebase Live Database Security Rule for the path 'user/{uid}' is as follows
"rules": {
"user": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid && !data.exists() "
}
}
}
in the typescript I attempt to read 'user/{uid}' for some user.
//Firebase and firebase-fcuntions import
const firebase = require("firebase");
const functions = require('firebase-functions');
//Reference to root of database
const rootdb = firebase.database();
//Read data
function foo(data, context){
return rootdb.ref('user').child(context.auth.uid).once('value')
.then(snap => //do stuff)
.catch( err => { console.log("Unsuccessful.")})
}
//Make call available from application using authentication
exports.enable_foo = functions.https.onCall( (data, context) => foo(data, context) );
The logs on firebase display:
Error: permission_denied at /user/XCRR0JK3xxZMoyoKzTIeQ2n1HcY2: Client
doesn't have permission to access the desired data. a...
and the "Unsuccesful" message for the catch path of execution prints.
What am I missing?
Edit 1
I should mention that the actual method, as opposed to the minimum working example above, does check for the user being logged in and prints the user auth.uid so I can confirm the user is logged in.
//Firebase and firebase-fcuntions import
const firebase = require("firebase");
const functions = require('firebase-functions');
//Reference to root of database
const rootdb = firebase.database();
//Read data
function foo(data, context){
// Checking that the user is authenticated.
if (!context.auth) {
console.log("No authentication.")
throw new functions.https.HttpsError('Authentication', "You are not authorized to execute this function." );
}
console.log( context.auth.uid )
return rootdb.ref('user').child(context.auth.uid).once('value')
.then(snap => //do stuff)
.catch( err => { console.log("Unsuccessful.")})
}
//Make call available from application using authentication
exports.enable_foo = functions.https.onCall( (data, context) => foo(data, context) );
When I execute this function the {uid} of the user shows up in the logs when I print it.
Edit 2
Replacing the 'firebase' requirement by "firebase-admin" appears to "fix" the issue, that is to say it allows the read.
I have a security concern with this, namely that users who are authenticated and DO have access to a resource are denied said resource if I use the "firebase" requirement. Needing the full access "firebase-admin" to allow a user to access(read or write) a resource seems over kill and unintended.
So, I suppose the question now is: is this intended behaviour?
When your client app invokes a callable function, the identity of the authenticated user is passed along to the function, but that doesn't mean the function is somehow automatically initializing any other modules with the user's credentials. So, if you import the Firebase client SDK, it is effectively running in unauthenticated mode (if it works at all - it was meant to run in browser environments, not nodejs).
Since backend code runs in a privileged environment that the user can't modify, it's typically said to be safe to run any code, since you wrote it, and you know exactly what it does. That's why it's recommended to user firebase-admin for access, which is meant primarily for backends and not client apps.
Now, if your concern is that the user might trigger the function and try to make it do something on their behalf that they shouldn't be able to do, you will have to write some code for that. The typical choice is to validate that the UID is allowed to do what the function is going to do. This means duplicating the checks of any security rules that would normally be used to protect the database.
Your other choice, which is going to suffer in performance, is to use the databaseAuthVariableOverride to tell the Admin SDK that it should use a different UID to access Realtime Database. Then it will respect all security rules. The problem here is that you have to init and then delete the firebase-admin instance for each request so that you don't leak memory. I show an example of this in another answer that uses an HTTP function to receive an auth ID token, validate its UID, and use the UID to init the SDK. Since you are using a callable type function, the validation has already been done for you, so all you have to do is use the given UID from the function in the same way.
According to the Firebase documentation
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"
}
}
}
}
These rules give anyone, even people who are not users of your app, read and write access to your database.
{
"rules": {
".read": true,
".write": true
}
}
These rules don't allow anyone read or write access to your database.
{
"rules": {
".read": false,
".write": false
}
}
Make sure that your code is reading the security rules for the chosen database, such as Cloud Firestore database or Firebase Realtime Database, which are totally distinct databases and use different security rules to control access. Also, check if your user is first authenticated before fetching data from the DB.

Access Azure Batch from an Azure Function

I'm trying to use a Service Principle to access a Batch pool from an Azure Function and running into authentication issues that I don't understand. The initial login with the Service Principle works fine, but then using the credentials to access the batch pool returns a 401.
Below is a condensed version of my code with comments at the key points
module.exports.dispatch = function (context) {
MsRest.loginWithServicePrincipalSecret('AppId', 'Secret', 'TennantId', function(err, credentials){
if (err) throw err;
// This works as it prints the credentials
context.log(credentials);
var batch_client = new batch.ServiceClient(credentials, accountUrl);
batch_client.pool.get('mycluster', function(error, result){
if(error === null)
{
context.log('Accessed pool');
context.log(result);
}
else
{
//Request to batch service returns a 401
if(error.statusCode === 404)
{
context.log('Pool not found yet returned 404...');
}
else
{
context.log('Error occurred while retrieving pool data');
context.log(error);
}
//'Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly.
context.res = { body: error.body.message.value };
context.done();
}
});
});
};
How can the initial login with a service principle work no problem, but then the credentials it returns not be able to access the batch pool?
The actual error says to check the auth header on the request, which I can see and the Authorisation header isn't even present.
I've triple checked the Active Directory access control for the batch account the App ID and secret are the ones belonging to the owner of the batch account. Any ideas what to try next?
The credentials expected by the Azure Batch npm client aren't the Azure Active Directory credentials/token, but the keys for the batch account. You can list your keys using the Azure CLI with a command like the following:
az batch account keys list -g "<resource-group-name>" -n "<batch-account-name>"
sample here
Then you can create the credentials parameter with those keys:
var credentials = new batch.SharedKeyCredentials('your-account-name', 'your-account-key');
You could still involve a Service Principal here if you wanted to store your batch keys in something like Key Vault, but then your code would be:
Get Service Principal auth against key vault to fetch name and key
Use name and key to create credentials
You cannot use the same OAuth token returned from the Azure Resource Management endpoint with Batch. Assuming your service principal has the correct RBAC permissions, auth with the Azure Batch endpoint: https://batch.core.windows.net/ instead (assuming you are using Public Azure).
You do not need to get the shared key credentials for the Batch account, credentials via AAD should be used instead if you are using an AAD service principal.
I happened to run across this same issue and I didn't have the option of using SharedKeyCredentials so I wanted to share my solution in case anyone else finds it helpful.
As fpark mentions, we need to get an OAuth token to use with Batch instead of the default Azure Resource Management. Below is the original code posted by Mark with the minor modification needed to make it work with Batch:
module.exports.dispatch = function (context) {
let authOptions = {tokenAudience: 'batch'};
MsRest.loginWithServicePrincipalSecret('AppId', 'Secret', 'TennantId', authOptions, function(err, credentials){
if (err) throw err;
// This works as it prints the credentials
context.log(credentials);
var batch_client = new batch.ServiceClient(credentials, accountUrl);
batch_client.pool.get('mycluster', function(error, result){
if(error === null)
{
context.log('Accessed pool');
context.log(result);
}
else
{
//Request to batch service returns a 401
if(error.statusCode === 404)
{
context.log('Pool not found yet returned 404...');
}
else
{
context.log('Error occurred while retrieving pool data');
context.log(error);
}
//'Server failed to authenticate the request. Make sure the value of Authorization header is formed correctly.
context.res = { body: error.body.message.value };
context.done();
}
});
});
};

firebase server Authenticate with admin privileges

I am trying to setup firebase server using nodejs as discussed Here. However my code is not able to read or wite data from/to any tables using admin privileges even the ones with public read and write access.
This is my code from the server side:
var firebase = require('firebase');
firebase.initializeApp({
serviceAccount: "./App.json",
databaseURL: "https://userPosts.firebaseio.com"
});
//
var db = firebase.database();
var ref = db.ref("testPosts");
ref.once("value", function(snapshot){
console.log(snapshot.val());
}, function(error){
console.log(error);
});
The code below is how the security is set on the server:
"rules": {
"users": {
"$uid": {
".read": "auth != null && auth.uid == $uid",
".write": "auth != null && auth.uid == $uid"
}
},
"testPosts": {
".read": "auth != null",
".write": "auth != null"
}
}
If I comment the serviceAccount line on the server side, the data will be read with no issue and access will be denied when I try to access a node that it does not have access ( which is normal behaviour).
My understanding of Firebase admin privileges is that I should have access to all table nodes regardless of the access privilege.
I would really appreciate if someone can help with this issue. I have been stuck on it for a while now.
-----------------------Edit----------------
I have enabled debugging by adding
firebase.database.enableLogging(true);
and I get the following error:
p:0: Listen called for /posts default
p:0: Making a connection attempt
p:0: Failed to get token: Error: Error refreshing access token: invalid_grant (Invalid JWT: Token must be a short-lived token and in a reasonable timeframe)
p:0: data client disconnected
p:0: Trying to reconnect in 77.64995932509191ms
0: onDisconnectEvents
Can anybody help with the error and what it really means?
After some diggings, it appears that my server time was the problem.
The time of my server was not in sync with the Network Time Protocol (NTP).
In case someone is experiencing the same issue, the following link can be useful.

Resources