I am using Firebase admin auth as authentication and authorization mechanism (client send token, server will validate and check users' roles contained in the custom claims) in my Cloud Run running graphql server. The admin auth module will throw error whenever it tries to call verifyIdToken. Calling admin auth methods from cloud functions work flawlessly though.
FirebaseAuthError: Must initialize app with a cert credential or set your Firebase project ID as the GOOGLE_CLOUD_PROJECT environment variable to call verifyIdToken().
I have tried using app engine service account (the same as the one used by cloud functions) and creating new one with firebase admin role as my cloud run's service account but resulted in no avail. I am able to make it run by providing the credentials file (generated from firebase console) in my Dockerfile and set the env variable GOOGLE_APPLICATION_CREDENTIALS, but I want to use that as the last resort beside it's very unsightly.
Below is my Apollo server's context function
import { Context, ContextFunction } from 'apollo-server-core';
import { ExpressContext } from 'apollo-server-express/dist/ApolloServer';
import { firebase } from '../config';
import { batchLoaders } from './batchLoaders';
export const context: ContextFunction<ExpressContext, Context> = async ({
req
}) => {
const token = req.headers.authorization || '';
const defaultContext = { batchLoaders };
if (token.length === 0) return defaultContext;
try {
const { uid } = await firebase.auth.verifyIdToken(token);
const user = await firebase.auth.getUser(uid);
return { uid, claims: user.customClaims, ...defaultContext };
} catch (err) {
console.error(err);
return defaultContext;
}
};
And the file in which it imports the firebase module from.
const firebaseApp = initializeApp();
const auth = firebaseApp.auth();
const firestore = firebaseApp.firestore();
export const firebase = {
auth,
firestore,
refs: {
events: firestore.collection('_events'),
versions: firestore.collection('_versions')
}
};
Isn't the same service account supposed to be able to access the admin auth resources?
Dang my bad, The error message says it all, I need to provide GOOGLE_CLOUD_PROJECT in the cloud run env vars.
adding GOOGLE_CLOUD_PROJECT as an environment variable on cloud run also solved my problem
Related
Edit: I forgot to mention that I am using Firebase cloud functions to host the site.
I am having trouble successfully initializing the NodeJS SDK with an authorization strategy that combines my service account file together with Google Application Default Credentials as detailed here: https://firebase.google.com/docs/admin/setup#initialize-sdk
I set the environment variable GOOGLE_APPLICATION_CREDENTIALS to the file path of the JSON file that contains my service account key like so:
Windows Powershell method:
$env:GOOGLE_APPLICATION_CREDENTIALS="C:\Users\XXX\firebase\service-account.json"
Then, I created my server backend utils file to initialize the SDK like so:
server/utils/firebase.ts:
// Purpose: initialize firebase and export it with service credentials
import { initializeApp } from 'firebase-admin/app'
// Add other libraries here (ie, auth)
import { getFirestore } from 'firebase-admin/firestore'
import { getAuth } from 'firebase-admin/auth'
// Initialize firebase with service credentials established with Google application default credentials
// Reference: https://firebase.google.com/docs/admin/setup#windows
const app = initializeApp()
const firestore = getFirestore()
const auth = getAuth()
export { app, firestore, auth }
Unfortunately, when I try to fetch a document from Firestore, I get the following server error. I thought I could implicitly initialize the SDK without parameters in the initializeApp() ?
{
"url": "/api/users/5cnu6iDlTsa5mujH2sKPsBJ8OKH2/profile",
"statusCode": 500,
"statusMessage": "",
"message": "Unable to detect a Project Id in the current environment. \nTo learn more about authentication and Google APIs, visit: \nhttps://cloud.google.com/docs/authentication/getting-started",
"stack": "<pre><span class=\"stack internal\">at GoogleAuth.findAndCacheProjectId ...snipped...
}
However, if I initialize the SDK like the below code, I have no problem fetching from Firestore:
// Purpose: initialize firebase and export it with service credentials
// Add other libraries here (ie, auth)
import { initializeApp, cert } from 'firebase-admin/app'
import { getFirestore } from 'firebase-admin/firestore'
import { getAuth } from 'firebase-admin/auth'
import serviceAccount from '../../service-account.json' //<--- app root location
const app = initializeApp({
credential: cert(serviceAccount)
})
const firestore = getFirestore()
const auth = getAuth()
export { app, firestore, auth }
I suppose I can go with the latter method, but I think the former is more secure?
I'm attempting to use a Firebase Cloud Function to create signed download URLs for files stored in a Storage Bucket. Using the snippet below on my local machine, I'm able to access cloud storage and generate these URLs.
/* eslint-disable indent */
import * as functions from "firebase-functions";
import * as admin from "firebase-admin";
// eslint-disable-next-line #typescript-eslint/no-var-requires
const serviceAccount = require("./test-serviceAccount.json");
admin.initializeApp();
const storage = admin.storage();
const bucket = storage.bucket();
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
}, "firestore");
export const getFile = functions.https.onRequest(async (request, response) => {
const [files] = await bucket.getFiles();
const fileNames: string[] = [];
files.forEach(async (file) => {
console.log(file.name);
const url = await file.getSignedUrl(
{
version: "v2",
action: "read",
expires: Date.now() + 1000 * 60 * 60 * 24,
}
);
fileNames.push(String(url));
if (files.indexOf(file) === files.length - 1) {
response.send(JSON.stringify(fileNames));
}
});
});
However after deploying to Cloud Functions I get an error when I call the function saying:
Error: could not handle the request
and the following message is logged in the functions console:
Error: The caller does not have permission
at Gaxios._request (/workspace/node_modules/gaxios/build/src/gaxios.js:129:23)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async Compute.requestAsync (/workspace/node_modules/google-auth-library/build/src/auth/oauth2client.js:368:18)
at async GoogleAuth.signBlob (/workspace/node_modules/google-auth-library/build/src/auth/googleauth.js:655:21)
at async sign (/workspace/node_modules/#google-cloud/storage/build/src/signer.js:97:35)
I've tried using and not using a .json service account key and made sure that the service account has permissions (it has Service Account Token Creator, Storage Admin, and Editor roles at the moment).
I also read this issue relating to the python SDK for storage, but it seems to have been resolved. The workaround mentioned in that issue (using a .json service account token) also didn't resolve the permissions errors.
After working with Firebase support - here's what worked for me:
import { initializeApp, applicationDefault } from 'firebase-admin/app';
initializeApp({
credential: applicationDefault(),
projectId: '<FIREBASE_PROJECT_ID>',
});
Specifying the projectId in the init call seems to have resolved the issue.
Signed url means it is signed for (or, accessible to) any particular user for particular span of time, maximum 7 days. If you trying to get it for unauthenticated users, it may show such error(s).
It's better to use getDownloadURL() for unauthenticated users. getSignedUrl() should to used for authenticated users only.
I've a project in which I used to authenticate the users with firebase-auth.In my project users can not create their accounts on their own.Only admin have the privilege to add the user accounts.
In order to use onAuthStateChanged() function I must use firebase-auth in my page.But the issue is because of using firebase-auth on client side one can esaily create accounts by running createUserWithEmailAndPassword() function on the console without having the admin privilege.
Now how can I restrict the people from using createUserWithEmailAndPassword() function on client side?
The only way you can stop clients apps from creating accounts is to disable all authentication providers for your project in the Firebase console. You could write an auth onCreate Cloud Function that attempts to figure out if a new account was created by client or admin code if you want to try to delete it immediately.
I think you can add a claim once the user is added, via a cloud function, which requires authorization, so that if the user doesn't have that claim he can't use the app or can't login.
In 2022 with Firebase Auth with Identity Platform and blocking functions, we can accomplish that the following way:
Create an HTTP function that receives email, password and displayName, and creates user using firebase-admin:
import { https } from 'firebase-functions';
import { getAuth } from 'firebase-admin/auth';
import cors from 'cors';
const auth = getAuth();
// Register an HTTP function with the Functions Framework
export const signupUser = https.onRequest((req, res) => {
const options = {
origin: 'http://localhost:3000'
};
cors(options)(req, res, () => {
console.log('all good');
auth
.createUser({
email: 'example#email.com',
emailVerified: false,
password: 'secretPassword',
displayName: 'John Doe',
disabled: false,
})
.then((userRecord) => {
// See the UserRecord reference doc for the contents of userRecord.
console.log('Successfully created new user:', userRecord.uid);
})
.catch((error) => {
console.log('Error creating new user:', error);
});
// Send an HTTP response
res.send('OK');
});
});
Modify response and origin in CORS as you need.
Now create a blocking beforeCreate function and check for user's display name, if there is no display name, throw an error:
import { auth } from "firebase-functions";
import { initializeApp, applicationDefault } from 'firebase-admin/app';
import { getAuth } from 'firebase-admin/auth';
import postmark from 'postmark';
const app = initializeApp({
credential: applicationDefault(),
projectId: 'your_project_id',
});
const tnc = getAuth(app);
export const signUp = auth
.user().beforeCreate((user, context) => {
if (!user.displayName) {
throw new auth.HttpsError('permission-denied');
}
});
This will work because there is no way to include "display name" when signing up via client side
So you, in short, point is to create a Cloud Function that will register users and make sure to add the check to beforeCreate for something that you know is only possible to do on server-side via firebase-admin sdk.
EDIT: CORRECTION
Just found out you can now disable client side signup from Firebase Console if you have Auth + Identity Platform
I've set some config variables using the command firebase functions:config:set algolia.appid="app_id" algolia.apikey="api_key", but how do I utilize them in my Flutter app? I have firebase_core installed.
In TypeScript you would write the following:
import * as admin from 'firebase-admin';
admin.initializeApp();
const env = functions.config();
console.log(env.algolia.appid);
But what about in Dart and Flutter?
Thanks
The configuration variables you set through firebase functions:config:set are (as the command implies) available only in Cloud Functions. They're not in any way propagated to client-side application by this command. In general that'd be an anti-pattern, as such configuration variables are often used for keeping trusted credentials.
If you have a use-case where the value needs to be available in the client-side application too, you have a few ways to do that:
Create an additional Cloud Functions endpoint where you expose the value of the configuration variable. Typically this would be a HTTPS or Callable function, which you then call from your client-side code.
Push the value into another place where your application code can read it from at the same time that you call firebase functions:config:set. This could be a configuration file of your app, or even a .dart file that you generate.
I also ran into this problem and found myself on this S.O. thread. I tried following Frank van Puffelen's suggestion above.
In functions/.runtimeconfig.json:
{
"algolia": {
"appid": "ID",
"apikey": "KEY"
},
"webmerge": {
"key": "KEY",
"secret": "SECRET",
"stashkey": "STASH_KEY"
},
}
In functions/index.ts:
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
. . .
const cors = require('cors')({origin: true})
const envObj = functions.config()
. . .
export const getEnv = functions.https.onRequest((req, resp) => {
cors(req, resp, () => resp.status(200).send(JSON.stringify(envObj)));
});
. . .
NOTE: I used the cors package to get around CORS errors when working locally. I would get these errors when localhost:5000 (Emulator hosting) called localhost:5001 (Emulator functions).
In web_flutter/main.dart:
Future<Map<String, dynamic>> fetchEnv(String functionsURL) async {
var response = await http.get('${functionsURL}/getEnv');
return json.decode(response.body);
}
Future<void> main() async {
try {
var functionsURL = 'FUNCTIONS_URL';
var app = fb.initializeApp(firebase app details);
if (window.location.hostname == 'localhost') {
app.firestore().settings(Settings(
host: 'localhost:8080',
ssl: false,
));
functionsURL = 'http://localhost:5001';
}
var env = await fetchEnv(functionsURL);
var searchClient = Algolia.init(
applicationId: env['algolia']['appid'],
apiKey: env['algolia']['apikey']);
runApp(MyApp(
repository: Repository(app.firestore(), searchClient),
authentication: Authentication(app.auth())));
} on fb.FirebaseJsNotLoadedException catch (e) {
print(e);
}
}
Once I confirmed that this was working locally, I was able to use firebase functions:config:set to set this data in the live Functions environment and deploy my updated hosting and functions with firebase deploy.
I'm trying to send the verification email after the user is created. Since there's no way on Firebase itself, I'm trying it with cloud functions.
I cannot really find a lot of documentation about it. What I tried to do so far is:
exports.sendEmailVerification = functions.auth.user().onCreate(event => {
return user.sendEmailVerification()
});
But I get the error that user is not defined.
How can I create this function?
Thanks!
There are two possibilities to send an "email verification" email to a user:
The signed-in user requests that a verification email be sent. For that, you call, from the front-end, the sendEmailVerification() method from the appropriate Client SDK.
Through one of the Admin SDKs, you generate a link for email verification via the corresponding method (e.g. auth.generateEmailVerificationLink() for the Node.js Admin SDK) and you send this link via an email sent through your own mechanism. All of that is done in the back-end, and can be done in a Cloud Function.
Note that the second option with the Admin SDKs is not exactly similar to the first option with the Client SDKs: in the second option you need to send the email through your own mechanism, while in the first case, the email is automatically sent by the Firebase platform
If you'd like that ability to be added to the Admin SDK, I'd recommend you file a feature request.
This is how I implemented it successfully using Firebase cloud functions along with a small express backend server
Firebase Cloud function (background) triggered with every new user created
This function sends a "user" object to your api endpoint
const functions = require('firebase-functions');
const fetch = require('node-fetch');
// Send email verification through express server
exports.sendVerificationEmail = functions.auth.user().onCreate((user) => {
// Example of API ENPOINT URL 'https://mybackendapi.com/api/verifyemail/'
return fetch( < API ENDPOINT URL > , {
method: 'POST',
body: JSON.stringify({
user: user
}),
headers: {
"Content-Type": "application/json"
}
}).then(res => console.log(res))
.catch(err => console.log(err));
});
Server Middleware code
verifyEmail here is used as middleware
// File name 'middleware.js'
import firebase from 'firebase';
import admin from 'firebase-admin';
// Get Service account file from firebase console
// Store it locally - make sure not to commit it to GIT
const serviceAccount = require('<PATH TO serviceAccount.json FILE>');
// Get if from Firebase console and either use environment variables or copy and paste them directly
// review security issues for the second approach
const config = {
apiKey: process.env.APIKEY,
authDomain: process.env.AUTHDOMAIN,
projectId: process.env.PROJECT_ID,
};
// Initialize Firebase Admin
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
// Initialize firebase Client
firebase.initializeApp(config);
export const verifyEmail = async(req, res, next) => {
const sentUser = req.body.user;
try {
const customToken = await admin.auth().createCustomToken(sentUser.uid);
await firebase.auth().signInWithCustomToken(customToken);
const mycurrentUser = firebase.auth().currentUser;
await mycurrentUser.sendEmailVerification();
res.locals.data = mycurrentUser;
next();
} catch (err) {
next(err);
}
};
Server code
// Filename 'app.js'
import express from 'express';
import bodyParser from 'body-parser';
// If you don't use cors, the api will reject request if u call it from Cloud functions
import cors from 'cors';
import {
verifyEmail
} from './middleware'
app.use(cors());
app.use(bodyParser.urlencoded({
extended: true,
}));
app.use(bodyParser.json());
const app = express();
// If you use the above example for endpoint then here will be
// '/api/verifyemail/'
app.post('<PATH TO ENDPOINT>', verifyEmail, (req, res, next) => {
res.json({
status: 'success',
data: res.locals.data
});
next()
})
This endpoint will return back the full user object and will send the verification email to user.
I hope this helps.
First view the documentation by Firebase here.
As the registration phase completes and result in success, trigger the following function asynchronously :
private void sendVerification() {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
user.sendEmailVerification().addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if (task.isSuccessful()) {
system.print.out("Verification Email sent Champion")
}
}
});
}
The user will now be provided with a verification Email. Upon clicking the hyper linked the user will be verified by your project server with Firebase.
How do you determine whether or not a user did verify their Email?
private void checkEmail() {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user.isEmailVerified()) {
// email verified ...
} else {
// error : email not verified ...
}
}
Sadly, you may not customize the content/body of your verification Email ( I have been heavily corresponding with Firebase to provide alternative less hideous looking templates ). You may change the title or the message sender ID, but that's all there is to it.
Not unless you relink your application with your own supported Web. Here.
Since the release of the Version 6.2.0 of the Node.js Admin SDK on November 19, 2018 it is possible to generate, in a Cloud Function, a link for email verification via the auth.generateEmailVerificationLink() method.
You will find more details and some code samples in the documentation.
You can then send an email containing this link via Mailgun, Sendgrid or any other email microservice. You'll find here a Cloud Function sample that shows how to send an email from a Cloud Function.
If you want to let Admin SDK do it, as of now there is no option other than generating the email verification link and sending with your own email delivery system.
However
You can write a REST request on cloud functions and initiate the email verification mail this way.
export async function verifyEmail(apiKey : string, accessToken : string) {
// Create date for POST request
const options = {
method: 'POST',
url: 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/getOobConfirmationCode',
params: {
key: apiKey
},
data: {
requestType : "VERIFY_EMAIL",
idToken : accessToken
}
};
return await processRequest(options); //This is just to fire the request
}
As soon as you signup, pass the access token to this method and it should send a mail to the signup user.
apiKey : Is the "Web API key" listed in General tab of your project settings in firebase console
access token : Access token of the current user (I use signup rest api internally so there is an option to request token in response)