Firebase multiple real time databases, custom auth issues - javascript

I'm attempting to use multiple real time databases for my single project in Firebase. For my setup, I'm generating tokens and sending to users from a server that is not part of Google's servers.
Everything works so far for the default database, but issues arise when attempting access the second real time database. I have read that it might be because you have to generate a token for each app instance, but to avoid becoming a difficult process, is it possible to generate a single token that can access all databases?
On my backend server we do this to generate a token:
initializeApp({
credential: admin.credential.cert(FirebaseCredentials),
});
usually the next line is a databaseURL. What is passed the service account JSON file.
Is there an easy way to allow a user to use one token to access all databases if we had 10 of them or is the solution to initalizeApp and specify a different database each time with 10 different tokens if the data is spread across 10 different real time databases.
Client side I used the generated token to sign up. The generated token does not specify a databaseURL and appears to use the default database. When I attempt to use the second database, I tried to follow the multi database instructions.
const app2 = initializeApp({ databaseURL: '...'}, 'app2');
const db = getDatabase(app2);
set(ref(db, 'users'), {
user: 'me',
});
When I use that second one, I get hit with permissions denied. The rules are identical between rtdbs. I'm guessing its because my original app config token is for the default database and not for the new one? Is it possible to somehow use a single generated token for all databases?
Firebase Rules for both databases.
{
"rules": {
".read": "auth.token.chatEnabled === true",
".write": "auth.token.chatEnabled === true"
}
}
Here is how I use the token on client side. I simply pass the token back from the server. Client uses the Javascript SDK to initialize app with the firebase config generated from initial startup.
Then I do:
import { initializeApp } from "firebase/app";
import { getDatabase, ref, set } from 'firebase/database';
const firebaseConfig = {
... the config
};
const app = initializeApp(firebaseConfig);
const auth = getAuth();
await signInWithCustomToken(auth, token);
const app2 = initializeApp({
databaseURL: 'secondatabaseurl'
}, 'app2');
const db = getDatabase(app2);
set(ref(db, 'users'), {
user: 'me',
});
I believe my issue arises because when I go to call the second app, the token generated appears to be only usable for the first app or default config.
If all of my databases are on a single project, do I have to create a separate app for each new database? If so, would that mean I need a separate token for each app instance or to sign into each app instance with the same token?
Thank you for your time.

Not the answer, but I can initialize 2 different apps.
This works ->
const secondaryApp = initializeApp({ appconfig2 }, 'secondaryApp');
const auth = getAuth(secondaryApp);
const db = getDatabase(secondaryApp);
await signInWithCustomToken(auth, token);
set(ref(db, 'users'), {
user: 'me',
});
This does not work ->
const secondaryApp = initializeApp({ appconfig2 }, 'secondaryApp');
const auth = getAuth(secondaryApp);
await signInWithCustomToken(auth, token);
const db = getDatabase(secondaryApp);
set(ref(db, 'users'), {
user: 'me',
});
// now try a second database
const db2 = getDatabase();
set(ref(db2, 'users'), {
user: 'me',
});
From my understanding, getDatabase() pulls from the default initalizedApp. It appears that once a user has authenticated using one of the initializedApps (in this case the user initialized with secondaryApp), those authenticated details are not shared with other apps at least I'm not exactly sure how to share them with all apps declared. Even if the app configs have identical api keys, auth domains, etc but differ on the DatabaseURL, they do not somehow just connect.
Ideally once a user has authenticated, it should be that they have access to all databases in the single project/app, but this does not seem to be the case here.
The last response here might be the answer ->
Got permission denied when writing to the second instance of Firebase realtime database
So in order to access another database, a user must authenticate individually?

Okay so that link at the end was the answer, I just didn't expect to have to do this.
If you specify auth for each database, since the databases are declared in different apps, you have to authenticate the user with each one before being able to read/write.
In other words, to make it work I had to signInWithCustomToken per each database instance.
const app1 = initializeApp(config1);
const app2 = initializeApp(config2);
const auth1 = getAuth(app1);
const auth2 = getAuth(app2);
await signInWithCustomToken(auth1, token);
await signInWithCustomToken(auth2, token);
then you can use each app instance to getDatabase(app1/app2).
Maybe the reason for this is that people can use completely different apps. Maybe there is some way in future to make it so that one app with multiple sharded real time databases do not require authentication into each app database. Maybe it would be instead of declaring a new app for each sharded db, it should be some other helper function.

Related

Vue 3 Firebase Auth get *any* user data by id? [duplicate]

This question already has an answer here:
How to get FirebaseUser from a uid?
(1 answer)
Closed 8 months ago.
New to vue/firebase. I was able to lookup how to pull up “current user” data from auth no problem but trying to write a js composable that I can pass in any user id (not necessarily current user) and it will return the user object or at least displayName.
All the docs/vids I can find on the topic reference getting info on *current user *only not another user. From the Google Docs it says I should be able to do this in the "Retrieve user data" section. Closest model to Vue code-wise seems to be “Node.Js” but it isn't working.
Here's what I've got in getUserById
import { getAuth } from 'firebase/auth'
const getUserById = (u) => { // u = user id
const userData = null
getAuth()
.getUser(u)
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log(`Successfully fetched user data: ${userRecord.toJSON()}`);
userData = userRecord
})
.catch((error) => {
console.log('Error fetching user data:', error);
});
return { userData }
}
export default getUserById
The error I get is getUser is not a function. I tried adding getUser to the import but same error.
There is no way to look up information about just any user by their UID in the client-side APIs of Firebase as that would be a security risk. There is an API to look up a user by their UID in the Admin SDK, but that can only be used on a trusted environment (such as your development machine, a server you control, or Cloud Functions/Cloud Run), and not in client-side code.
If you need such functionality in your app, you can either wrap the functionality from the Admin SDK in a custom endpoint that you then secure, or have each user write information to a cloud database (such as Realtime Database or Firestore) and read it from there.
Also see:
How to get FirebaseUser from a uid?
Firebase get user by ID
Is there any way to get Firebase Auth User UID?

How to impersonate a Google Workspace account through ADC on Google Cloud Functions?

What I'm trying to do is to:
Create a service account
Give that service account Domain Wide Delegations
Use the Application Default Credentials to impersonate a Google Workspace user and access it's email with gmail API
My code works fine in local development, by using the credential key json file generated for the Service Account mentioned in step 1, through Application Credentials Default mechanism (I have set up the environment variable GOOGLE_APPLICATION_CREDENTIALS to the path of the credentials.json)
import {google} from 'googleapis'
const scopes = [
'https://www.googleapis.com/auth/gmail.readonly',
'https://www.googleapis.com/auth/gmail.modify',
];
const auth = new google.auth.GoogleAuth({
clientOptions: {
subject: 'user#email.com',
},
scopes,
});
const gmail = google.gmail({
version: 'v1',
auth,
});
const list = await gmail.users.messages.list({
userId: 'me',
maxResults: 10,
});
The problem is AFAIK, in local environment (aka not GCE) GoogleAuth uses JWT and in GCE it uses Compute Auth method, where a subject can't be configured or if configured is ignored.
So that's why when deploying the Cloud Function it throws an error about Precondition Check Failed and nothing else more specific.
In my limited knowledge and research I think the solution would be to somehow convert the Compute Auth -> JWT with a subject defined.
The current solution I have implemented and works, consists in saving the credentials.json into the Google Secret Manager:
// Acquire credentials from secret manager:
const secret = await getApiKeyFromSecretManager('SECRET_NAME');
const jsonCreds = JSON.parse(Buffer.from(secret).toString());
// Create the JWT
const auth = google.auth.fromJSON(jsonCreds);
auth.subject = 'user#email.com';
auth.scopes = scopes;
But I'm not really comfortable having to access or save the credentials in the Secret Manager, as I think the solution is not as elegant as it could be.

Limit pages to certain user type Nuxt & Firebase Firestore (Role-Based Authorization)

need some advice here.
My Nuxt & Firebase/Firestore web app will have 3 different type of users:
subcontractor
contractor
worker
First, I want my users, whenever they login, they will log into page related to their user type.
e.g: subcontractor login push to /subcontractor, contractor login push to /contractor etc etc.
I also want the user can only see pages related to their types. (user A only see /A & /Atwo, user B can only see /B & /Btwo, user C, can only see /C & /Ctwo etc etc..)
I want to avoid using cloud functions if can, as from what I understand, you cannot deploy your app in the free plan if your app has cloud functions in it.
Anyway, is below the right way to do it?
Create in firestore, Users document that contains details of user type,
e.g: "userType: subcontractor"
In the middleware, do the logic, based on user type.
(in my case, I need to have 3 diff middleware js file (isSubcontractor.js, isContractor.js, isWorker.js)
add middleware: "the-middleware file", inside my page
If its correct, how to do step 1 & 2?
Is there any articles or real-life application source code that explain briefly what I wanted?
Beginner here. Already gone thru here and there around the internet but can't quite find the answer that I wanted :(
Custom Claims are definitely an option but that would require Cloud functions or a server. Yes, you can store user type in their Firestore document and check it before the page renders or whenever required. However, you must make sure only authorized users can change their role.
The flow would be as simple as:
User logs in
Reading their role from Firestore document
Redirecting to relevant page
import { getAuth, signInWithEmailAndPassword } from "firebase/auth";
import { getFirestore, doc, getDoc } from "firebase/firestore";
const auth = getAuth();
const firestore = getFirestore();
const login = async () => {
const { user } = await signInWithEmailAndPassword(auth, email, password);
// Reading user document
const docRef = doc(firestore, "users", user.uid);
const docSnap = await getDoc(docRef);
const { userType } = docSnap.data()
switch (userType) {
case 'contractor':
// redirect to /contractor
break;
case 'sub-contractor':
// redirect to /sub-contractor
break;
default:
// redirect to default page
break;
}
}
I also want the user can only see pages related to their types.
You can follow them same method in a server side middleware. First read userType and then check if user is authorized to visit the page. If not, redirect to any other page.
Best part of using Custom Claims is that you can read them in security rules of Realtime Database, Firestore and Storage as well. If you store user type in Firestore you cannot read that in security rules of any other Firebase service. Using Firestore also incurs additional charge for reading user's role every time. You need a Cloud function to set the custom claim only and not read the claim every time.

Create multiple apps at the same time and caching the reference to db

I want to make a use of Firebase cloud messaging I have nearly 1000 service account that means I got 1K app with me.
The service account can be initialised once
var foo = admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
})
to send the notification
foo.messaging().sendToDevice(fcm_token, message)
.then((response) => {
console.log('Successfully sent message:', response);
})
So I thought of caching the foo (saving a foo reference list) for all the 1K service account by initialising the app in the running memory. This works perfect
But in terms of scalability that's not the right way. so I thought of putting this in redis but data type of foo is FirebaseApp
could you please help in figuring out the way where I can catch the foo reference later use it.
Fcm official example
// Initialize the default app
admin.initializeApp(defaultAppConfig);
// Initialize another app with a different config
var otherApp = admin.initializeApp(otherAppConfig, 'other');
console.log(admin.app().name); // '[DEFAULT]'
console.log(otherApp.name); // 'other'
// Use the shorthand notation to retrieve the default app's services
var defaultAuth = admin.auth();
var defaultDatabase = admin.database();
// Use the otherApp variable to retrieve the other app's services
var otherAuth = otherApp.auth();
var otherDatabase = otherApp.database();

Administrator Views for a Firebase Web Application: How To

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

Categories