Firebase database access denied with permission set - node.js

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.

Related

Edit User's Custom Claims from Firebase

I am using firebase to generate JWT tokens to authorize access to a hasura graphql server.
I want an end user to have a callable firebase function that they can call from the app so they can change the x-hasura-role in their claims without changing other parts of their claims. I am guessing the best way to do this is to export the old custom user claims and set a new role inputted by the user.
PseudoCode:
exports.changeUserType = functions.https.onCall( async (data, context) => {
var userType = data.usertype;
// get the old user claims somehow
// check if user should be able to change their userType via a graphql query
...
// edit the user claims
return admin.auth().setCustomUserClaims(userType, {
'https://hasura.io/jwt/claims': {
'x-hasura-role': userType,
'x-hasura-default-role': 'orgdriver',
'x-hasura-allowed-roles': ['orgauditor', 'orgdriver', 'orgmanager', 'orgadmin', 'orgdirector'],
'x-hasura-user-id': user.uid // <-- from the old claims so user can't edit
}
});
If there is a better way to do this, maybe by grabbing a user's id from the auth database by checking who ran the function please tell me. Thank you in advance.
When a Firebase Authenticated user hits a Firebase Function, their uid is passed in through context. I would ensure they are authenticated first:
if (context.auth == undefined) {
throw new functions.https.HttpsError(
'failed-precondition',
'The user must be authenticated.',
);
}
Then I would grab their uid:
const uuid = context?.auth?.uid as string;
Then you can get their user using the firebase-admin library's getAuth():
// get user
const user = await getAuth().getUser(uuid);
Now finally you can set your new custom claim property:
// set the hasura role
return await getAuth().setCustomUserClaims(uuid, {
...user.customClaims,
'x-hasura-role': userType,
});
Be sure to import:
import { getAuth } from 'firebase-admin/auth';
In this way you can safely know the user is authenticated and a uid exists, then you can simply grab the user and all their existing claims, then when you go to update destructure all existing claims values, and update the one value you want.
In this way get all the user's old claims, ensure they are authenticated, retain all old claim properties, and update the one thing you want to update.
I hope that helps out!

NestJS: Authorization based on instances property best practice

I need authorization in NestJS based on instances property.
Ex. user can update only his own articles.
Is there another way despite defining the logic in each services? ( I know it is possible using CASL )
Not having a global guard will facility errors, and everything is authorized by default unless add logic on the service.
What about creating a function that takes the request, the model and the name of the proprety and use it wherever you want ?
const verifAuthorization = (
req: Request,
propName: string,
model: any
): void => {
const sender: User = req.user;
if (!sender) {
throw new BadRequestException("there is no user in the token");
}
if (!sender._id.equals(model[propName])) {
throw new UnauthorizedException();
}
};
Yes ! you will call it in every service you want to check the authorization in, but it will save you a lot of time and code

How to delete a user and their Firestore document in a callable cloud function

I'm creating a cloud function in firebase and need some help,I'm trying to delete a user form firebase and delete his document from firestore in one cloud function.
How can I make a batch job / transaction for both auth and firestore, lets say the user tries to delete his account but for some reason the user.delete() function doesn't work (lets say it's down on firebases side for that moment). The user would get en error message that we couldn't delete his account but when he tries to login again he would also get an error because his document doesn't exist.
I looked at the firebase extension to delete user data but it doesn't delete the user account and it seems to have the same problem.
Do I need to handle such edge case in the app/cloud-functions, is it something firebase should take care of or am I just getting something wrong?
Here is my code, if it would help:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
exports.deleteUser = functions.https.onCall(async (data, context) => {
try {
const uid = context.auth.uid;
const db = admin.firestore();
const collection = db.collection("users");
await collection.doc(uid).delete();
await admin.auth.deleteUser(uid); // what if this line fails?
return "success";
} catch (err) {
console.error(err);
return "error";
}
});
This line isn't doing what you think it's doing:
const user = await admin.auth().currentUser;
user is going to be undefined, because admin.auth() doesn't have a property called currentUser (it's an Auth object). The concept of "current user" doesn't exist on backend SDKs. It's a frontend concept only. What you do have, however, is a string uid which is the UID of the authenticated user that invoked the function.
If you want to use the Firebase Admin SDK to delete the user identified by the uid string, then you just need to call deleteUser(uid):
await admin.auth().deleteUser(uid);
By the way, the Delete User Data extension doesn't have to delete the user, because it works by responding to the user deleting their own account using the client SDK. That should actually be enough to make this work.

Spotify node web api - trouble with multiple users

I am working on an app that uses Spotify Node web API and having trouble when multiple users login into my application. I am successfully able to go through authentication flow and get the tokens and user ID after a user logs in. I am using the Authorization Code to authorize user (since I would like to get refresh tokens after expiration). However, the current problem is that getUserPlaylists function described here (FYI, if the first argument is undefined, it will return the playlists of the authenticated user) returns playlists of the most recently authenticated user instead of the user currently using the app.
Example 1: if user A logins in to the application, it will get its playlists fine. If user B logins in to the application, it also sees its own playlists. BUT, if user A refreshes the page, user A sees the playlists of the user B (instead of its own, user A playlists).
Example 2: user A logs in, user B can see user A's playlists just by going to the app/myplaylists route.
My guess is, the problem is with this section of the code
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
The latest user tokens override whatever user was before it and hence the previous user is losing grants to do actions such as viewing its own playlists.
Expected behavior: user A sees own playlists after user B logs in event after refreshing the page.
Actual behavior: user A sees user B's playlists after user B logged in and user A refreshes the page.
I am aware that I could use the tokens without using the Spotify Node API
and just use the tokens to make requests and it should probably be fine, however, it would be great to still be able to use the Node API and to handle multiple users.
Here is the portion of code that most likely has problems:
export const createAuthorizeURL = (
scopes = SCOPE_LIST,
state = 'spotify-auth'
) => {
const authUrl = spotifyApi.createAuthorizeURL(scopes, state);
return {
authUrl,
...arguments
};
};
export async function authorizationCodeGrant(code) {
let params = {
clientAppURL: `${APP_CLIENT_URL || DEV_HOST}/app`
};
try {
const payload = await spotifyApi.authorizationCodeGrant(code);
const { body: { expires_in, access_token, refresh_token } } = payload;
spotifyApi.setAccessToken(access_token);
spotifyApi.setRefreshToken(refresh_token);
params['accessToken'] = access_token;
params['refreshToken'] = refresh_token;
return params;
} catch (error) {
return error;
}
return params;
}
export async function getMyPlaylists(options = {}) {
try {
// if undefined, should return currently authenticated user
return await spotifyApi.getUserPlaylists(undefined, options);
} catch (error) {
return error;
}
}
Would appreciate any help on this. I am really excited about what I am making so it would mean a LOT if someone could help me find the issue...
You're on the right track. When you set your access token and refresh token, though, you're setting it for your entire application, and all users who call your server will use it. Not ideal.
Here's a working example of the Authorization Code Flow in Node: https://glitch.com/edit/#!/spotify-authorization-code
As you can see, it uses a general instance of SpotifyWebApi to handle authentication, but it instantiates a new loggedInSpotifyApi for every request to user data, so you get the data for the user who's asking for it.
If you want to use the above example, you can just start editing to "remix" and create your own copy of the project.
Happy hacking!

Firebase security rules for nodejs app

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();
})

Resources