I have this code in nextjs that is supposed to check if a token is valid then sign in the user.
const firebaseAdmin = require("firebase-admin");
const serviceAccount = require ('../secret.json');
export const verifyIdToken = async (token) => {
if (!firebaseAdmin.apps.length) {
firebaseAdmin.initializeApp({
// credential: firebaseAdmin.credential.cert(serviceAccount),
credential: firebaseAdmin.credential.applicationDefault(),
databaseURL: "rtdb.firebaseio.com",
});
}
return await firebaseAdmin
.auth()
.verifyIdToken(token)
.catch((error) => {
throw error;
});
};
I have the windows environment variables set as firebase recommends and switched to using the applicationDefault() since as I understand,
ADC can automatically find your credentials
Problem is the application works only locally. When I deploy the website, the token is not verified and creates errors. I am serving the NextJs app through a cloud function. How can I solve this.
The error is
auth/invalid-credential
Must initialize app with a cert credential or set your Firebase project
ID as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().
What the app is supposed to do is do a check server side to determine if a token is valid.
As below
export async function getServerSideProps(ctx) {
try {
const cookies = nookies.get(ctx);
const token = await verifyIdToken(cookies.token);
// the user is authenticated!
const { uid, email } = token;
return {
props: {
userData: {
email: email,
uid: uid,
},
},
};
} catch (err) {
console.log(err.code)
console.log(err.message)
return { props: {
} };
}
}
The auth/invalid-credential error message means that the Admin SDK needs to be initialized, as we can see in the Official Documentation.
The credential used to authenticate the Admin SDKs cannot be used to
perform the desired action. Certain Authentication methods such as
createCustomToken() and verifyIdToken() require the SDK to be
initialized with a certificate credential as opposed to a refresh
token or Application Default credential.
And for the ID token verification, a project ID is required. The Firebase Admin SDK attempts to obtain a project ID via one of the following methods:
If the SDK was initialized with an explicit projectId app option, the SDK uses the value of that option.
If the SDK was initialized with service account credentials, the SDK uses the project_id field of the service account JSON object.
If the GOOGLE_CLOUD_PROJECT environment variable is set, the SDK uses its value as the project ID. This environment variable is available for code running on Google infrastructure such as App Engine and Compute Engine.
So, we can initialize the Admin SDK with a service (and fulfill the second option); but, the first thing to do is authenticate a service account and authorize it to access Firebase services, you must generate a private key file in JSON format.
To generate a private key file for your service account you can do the following:
In the Firebase console, open Settings > Service Accounts.
Click Generate New Private Key, then confirm by clicking Generate Key.
Securely store the JSON file containing the key.
Once you have your JSON file, you can set a environment variable to hold your private key.
export GOOGLE_APPLICATION_CREDENTIALS="/home/user/Downloads/service-account-file.json"
And then, use it in your code like this:
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});
In the end I downloaded Gcloud tool and setting the GOOGLE_APPLICATION_CREDENTIALS environment variable from the tool worked. The function could then work with credential: firebaseAdmin.credential.applicationDefault(),
Related
we managed to assume a role, but it does not automatically refresh access, so after some time we get a timeout err.
We found not proper documentation on how to refresh credentials, so I was wondering if someone could provide a code example of how to setup this up properly.
We already tried with credentialDefaultProvider({ assumeRole }) in the DynamoDBClient constructor options, but it doesn't even call it.
AWS Secure Token Service is your friend for assuming a role and use its credentials.
// this example uses Typescript
import { AssumeRoleCommand, Credentials, STSClient } from '#aws-sdk/client-sts'
// get / refresh Credentials
const stsClient = new STSClient({ region: <YOUR_REGION> })
const data = await stsClient.send(
new AssumeRoleCommand({
RoleArn: <ROLE_ARN>,
RoleSessionName: <SOME_SESSION_NAME>,
DurationSeconds: <DURATION_IN_SECONDS>
}),
)
// now use the Credentials
// data.Credentials
You can also check if the Credentials are still valid before your requests:
if(new Date() > new Date(data.Expiration)){
// refresh Credentials
}
Useful links:
AWS Secure Token Service
AWS STS Example
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.
Recently searching the internet I found a good alternative to manage the secrets of my application created in node js with the help of hashicorp vault. I have investigated how it works and among the possible ways that this tool has to enter I found approle, which I consider an adequate form of authentication through my application. This form of authentication requires a role_id and a secret_id. The latter, as I see in the examples of the official vault page, needs an entity for its creation and then passes it to the application and in this way the application can receive the token to enter the vault. Currently I have this code in node js that receives a token wrapped with the secret_id to achieve access to the secrets with the role of the application:
//get the wrap token from passed in parameter
var wrap_token = process.argv[2];
if(!wrap_token){
console.error("No wrap token, enter token as argument");
process.exit();
}
var options = {
apiVersion: 'v1', // default
endpoint: 'http://127.0.0.1:8200',
token: wrap_token //wrap token
};
console.log("Token being used " + process.argv[2]);
// get new instance of the client
var vault = require("node-vault")(options);
//role that the app is using
const roleId = '27f8905d-ec50-26ec-b2da-69dacf44b5b8';
//using the wrap token to unwrap and get the secret
vault.unwrap().then((result) => {
var secretId = result.data.secret_id;
console.log("Your secret id is " + result.data.secret_id);
//login with approleLogin
vault.approleLogin({ role_id: roleId, secret_id: secretId }).then((login_result) => {
var client_token = login_result.auth.client_token;
console.log("Using client token to login " + client_token);
var client_options = {
apiVersion: 'v1', // default
endpoint: 'http://127.0.0.1:8200',
token: client_token //client token
};
var client_vault = require("node-vault")(client_options);
client_vault.read('secret/weatherapp/config').then((read_result) => {
console.log(read_result);
});
});
}).catch(console.error);
The problem is that I plan to upload the application in the cloud using docker and the idea is that the process of obtaining the secrets is automatic so I would like to know if when creating a token that lasts long enough that you only have the possibility of obtaining the secret_id of a role and saving it as environment variable is appropriate in this case or if there is any other alternative that can help me in automating this case.
Note: I don't plan to deploy in aws in this case.
I am trying to create a new table in BigQuery. I have followed these instructions https://codelabs.developers.google.com/codelabs/cloud-bigquery-nodejs/index.html?index=..%2F..index#9 and have my user and roles defined properly.
I created a node project, installed the google dependencies and have the following code:
const {BigQuery} = require('#google-cloud/bigquery');
const bigquery = new BigQuery({
projectId: 'myproject-develop-3fcb6',
private_key_id: "11111111111",
client_email: "myuser-bigquery-sa#myproject-develop-3fcb6.iam.gserviceaccount.com",
client_id: "212111112",
});
This is how im creating my dataset and table:
module.exports = {
createTable: ({ datasetId, tableId, schema, partitionBy}) => {
const options = { schema };
if (partitionBy) {
options.timePartitioning = {
field: partitionBy
};
}
return new Promise((resolve, reject) => {
resolve();
bigquery
.dataset(datasetId)
.createTable(tableId, options)
.then(results => resolve(results[0]))
.catch(err => {
handleError(err);
reject(err);
});
});
},
};
When i run my createTableFunction and pass in the data set name, table name, schema I get the following error immediately
ERROR: Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
How do I pass my default credentials to BigQuery so i can perform CRUD operations in node.js? Thanks
In the tutorial that you mentioned, this gcloud command creates a key.json:
gcloud iam service-accounts keys create ~/key.json --iam-account my-bigquery-sa#${GOOGLE_CLOUD_PROJECT}.iam.gserviceaccount.com
Then you can use the following code:
// Create a BigQuery client explicitly using service account credentials.
// by specifying the private key file.
const {BigQuery} = require('#google-cloud/bigquery');
const options = {
keyFilename: 'path/to/key.json',
projectId: 'my_project',
};
const bigquery = new BigQuery(options);
Authenticating With a Service Account Key File
I do not know where are you running your code, but in the tutorial is a line where you set the env variable therefore you do not need to authenticate using the key.json file in your code:
export GOOGLE_APPLICATION_CREDENTIALS="/home/${USER}/key.json"
GCP client libraries use a strategy called Application Default
Credentials (ADC) to find your application's credentials. When your
code uses a client library, the strategy checks for your credentials
in the following order:
First, ADC checks to see if the environment variable
GOOGLE_APPLICATION_CREDENTIALS is set. If the variable is set, ADC
uses the service account file that the variable points to. The next
section describes how to set the environment variable.
If the environment variable isn't set, ADC uses the default service
account that Compute Engine, Kubernetes Engine, Cloud Run, App Engine,
and Cloud Functions provide, for applications that run on those
services.
If ADC can't use either of the above credentials, an error occurs.
You can also pass credentials directly as parameters.
const {BigQuery} = require('#google-cloud/bigquery');
const bigQuery = new BigQuery({
projectId: "your-prject-id",
credentials: {...}, // content of json file
});
Thanks to #MahendraPatel comment.
I am trying to access Firestore using Firebase Admin on Node.js v8.4.0 in a Firebase Cloud Function running locally using firebase functions:shell on Windows 10.
firebase -v 4.2.1
"dependencies": {
"firebase-admin": "^6.0.0",
"firebase-functions": "^2.0.5"
}
After attempting to use firebase admin from my apps code, I attempted to run the quick start example in https://github.com/firebase/functions-samples/blob/master/quickstarts/uppercase-firestore/functions/index.js.
This is the actual code run:
'use strict';
// [START all]
// [START import]
// The Cloud Functions for Firebase SDK to create Cloud Functions and setup triggers.
const functions = require('firebase-functions');
// The Firebase Admin SDK to access the Firebase Realtime Database.
const admin = require('firebase-admin');
admin.initializeApp({})
// [END import]
// [START addMessage]
// Take the text parameter passed to this HTTP endpoint and insert it into the
// Realtime Database under the path /messages/:documentId/original
// [START addMessageTrigger]
exports.addMessage = functions.https.onRequest((req, res) => {
// [END addMessageTrigger]
// Grab the text parameter.
const original = req.query.text;
// [START adminSdkAdd]
// Push the new message into the Realtime Database using the Firebase Admin SDK.
return admin.firestore().collection('messages').add({original: original}).then((writeResult) => {
// Send back a message that we've succesfully written the message
return res.json({result: `Message with ID: ${writeResult.id} added.`});
});
// [END adminSdkAdd]
});
// [END addMessage]
// [START makeUppercase]
// Listens for new messages added to /messages/:documentId/original and creates an
// uppercase version of the message to /messages/:documentId/uppercase
// [START makeUppercaseTrigger]
exports.makeUppercase = functions.firestore.document('/messages/{documentId}')
.onCreate((snap, context) => {
// [END makeUppercaseTrigger]
// [START makeUppercaseBody]
// Grab the current value of what was written to the Realtime Database.
const original = snap.data().original;
console.log('Uppercasing', context.params.documentId, original);
const uppercase = original.toUpperCase();
// You must return a Promise when performing asynchronous tasks inside a Functions such as
// writing to the Firebase Realtime Database.
// Setting an 'uppercase' sibling in the Realtime Database returns a Promise.
return snap.ref.set({uppercase}, {merge: true});
// [END makeUppercaseBody]
});
// [END makeUppercase]
// [END all]
However, I still get the permission denied error.
This is the output I get:
firebase > makeUppercase({original:'alphabets'},{params:{documentId:'mydoc'}})
'Successfully invoked function.'
firebase > info: User function triggered, starting execution
info: Uppercasing mydoc alphabets
info: Function crashed
info: { Error: 7 PERMISSION_DENIED: Missing or insufficient permissions.
at Object.exports.createStatusError (C:\projects\myproject\functions\node_modules\grpc\src\common.js:87:15)
at Object.onReceiveStatus (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:1188:28)
at InterceptingListener._callNext (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:564:42)
at InterceptingListener.onReceiveStatus (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:614:8)
at callback (C:\projects\myproject\functions\node_modules\grpc\src\client_interceptors.js:841:24)
code: 7,
metadata: Metadata { _internal_repr: {} },
details: 'Missing or insufficient permissions.' }
My security rules are completely open but that did not resolve the error.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write;
}
}
}
I also thought that this might be an authentication issue so I have tried the following to initialize the app:
1
admin.initializeApp()
2
admin.initializeApp(functions.config().firebase);
3
var serviceAccount = require('path/to/serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://<DATABASE_NAME>.firebaseio.com'
});
The last one with my own credentials file and project configuration. All of these attempts still give me the missing permissions error.
Update:
I deployed these functions to the cloud and they seem to be working perfectly but when running locally, I'm still getting a Error:7 Permission Denied.
Update 2:
Set application default credentials using gcloud auth application-default login as per suggestion by #Doug Stevenson. Ensured environment variable GOOGLE_APPLICATION_CREDENTIALS is not set. Attempted the code in 1,2 and 3 above as well as 4 below with no success. Encountered the same error.
4
admin.initializeApp({
credential: admin.credential.applicationDefault(),
databaseURL: "https://myapp-redacted.firebaseio.com"
});
I hope that by now you've already resolved your issue. The same thing just happened to me and I'm sharing what it was in my case with the hope that it will help others.
We manage several firebase projects - productions, dev, staging etc.
we init the admin sdk with this:
let serviceAccount = require("../serviceAccountKey.json");
const databaseUrl = functions.config().environment.databaseurl
const storageBucket = functions.config().environment.storagebucket
const isDev = "true" === functions.config().environment.dev
// if dev environment
if (isDev) {
serviceAccount = require("../serviceAccountKey-dev.json");
}
admin.initializeApp({
projectId: functions.config().environment.projectid,
credential: admin.credential.cert(serviceAccount),
databaseURL: databaseUrl,
storageBucket: storageBucket
});
You know how you need to do
firebase functions:config:get > .runtimeconfig.json
for the emulation to work. Well, my runtimeconfig was containing the wrong configuration. I was loading my serviceAccountKey-dev, but I was trying to access a different project. The second I fixed my runtimeconfig - it worked for me.
I had the same issue and was resolved by going to the cloud console then granting the role Firebase Admin SDK admin service agent to the app engine service account which is in the following format {PROJECT_ID}#appspot.gserviceaccount.com.
Ultimately I just had to run gcloud auth application-default login to make sure I was logged in with the correct Google account.
It is required that the client, the firebase function, to have access to the resource, firebase firestore. Following the least privilege principle you would need to:
Create a role within Google Cloud IAM with the following permissions:
datastore.entities.get, datastore.entities.update.
Also in IAM, create a service account and assign it the recently created role.
Update your firebase function selecting the new service account.
I have not found a way to assign the service account while deploying with firebase-cli. Here a guide for configuring permissions of the functions https://cloud.google.com/functions/docs/securing/function-identity.