I am very new to Firebase and custom claims, so I hope someone can help.
I have an app that uses Firebase Auth as its authentication system. The app is built using Flutter. It is a subscription-based app, so the users must have an 'active' subscription status, in order to be logged in. The subscription information for each user, including the status, is stored in a Firestore collection called 'subscriptions'. Each user is assigned a document with their uid as its name when they create a subscription.
The way my app works is that a user will create an account on the WordPress website, using Firebase authentication. At this point, their user is created in Firebase Auth, but they have no assigned document in the 'subscriptions' Firestore collection. This is only created once they purchase a subscription with WooCommerce Subscriptions. I basically need them to be prevented from logging in if:
There is no 'subscriptions' Firestore document created for them (i.e. they have created an account but not purchased a subscription yet).
Their subscription status is not 'active', once they have purchased the subscription and the document has been created for them.
As far as I know, the best way to allow users to log in only if they have an active subscription, is to use custom claims. I've built the following two custom claims functions in javascript with VS Code, which are designed to check the subscriptions collection in Firestore for the user's subscription status, both when the document is created and if it is updated:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
// Claim when a user is created
exports.createUser = functions.firestore
.document("subscriptions/{subscription}")
.onCreate((snap, context) => {
// Get an object representing the document
const newValue = snap.data();
// Access a particular field
const userId = newValue.firebaseUid;
console.log("New subscription ${UserId} created!");
const status = newValue.status;
// Assign claims to the user
return admin.auth().setCustomUserClaims(userId, {status});
});
// Claim when a user is updated
exports.updateUser = functions.firestore
.document("subscriptions/{subscription}")
.onUpdate((change, context) => {
// Get an object representing the document
const updatedValue = change.after.data();
// Access a particular field
const userId = updatedValue.firebaseUid;
console.log("User subscription status updated!");
const status = updatedValue.status;
// Assign claims to the user
return admin.auth().setCustomUserClaims(userId, {status});
});
For some reason, these aren't working. They did successfully upload to my project in the Firebase console. However, they aren't adding a custom claim to the standard auth claim. I was expecting to see 'status: active/pending/cancelled' attached at the end of the claims map. They also aren't seemingly able to handle the case where no document exists for a user (i.e. when the user has only just created an account but hasn't yet subscribed).
Can someone show me why these aren't working and what I am doing wrong? How can I ensure the subscription status is added to my users' auth claims, so that I can guarantee they can only log in if they have an active subscription? Thanks!
I realised that the main problem with my code above is that I was using the wrong Firebase user ID getter value. I am using the plugins from TechCater and the value firebaseUid needed to be changed to firebase_user_id. In addition, I combined the functions into one, using onWrite instead of onCreate and onUpdate. The new/updated function, which works perfectly, looks like this:
const functions = require("firebase-functions");
const admin = require("firebase-admin");
admin.initializeApp();
// Claim when a user is updated
exports.onUpdateUsers = functions.firestore
.document("subscriptions/{subscription}")
.onWrite((change, context) => {
// Get an object representing the document
const updatedValue = change.after.data();
if (updatedValue == null) {
return;
}
// Access a particular field
const userId = updatedValue.firebase_user_id;
console.log("User subscription status created or updated!");
console.log("User ID: ", userId);
const status = updatedValue.status;
console.log("Status: ", status);
// Assign claims to the user
return admin.auth().setCustomUserClaims(userId, {status});
});
What's more, with respect to my comment in my original question about how to create a custom claim when a user had signed up but not purchased a subscription, I realised that all I needed to do was understand that when a user has created their account, but not purchased a subscription, the subscription status would be returned as null, so I just needed to check for the status being null as well as 'active' or other. The function now works exactly as I need it to.
Related
I want to run a Firebase cloud function everytime a user is charged by Stripe's subscription extension. Is there any event that is generated by that extension that can trigger a cloud function, such as a write to Firestore?
My scenario is that I want to generate a new document in an Orders collection whenever a user is successfully charged that references the corresponding User document.
I was able to solve this myself by borrowing some code from the official repo for Stripe's Firebase extension here: https://github.com/stripe/stripe-firebase-extensions/blob/next/firestore-stripe-subscriptions/functions/src/index.ts
I made my own Firebase function and called it handleInvoiceWebhook that validated and constructed a Stripe Invoice object, mapped it to a custom Invoice interface I made with the fields I cared about, then saved that to an invoices collection.
export const handleInvoiceWebhook = functions.https.onRequest(
async (req: functions.https.Request, resp) => {
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(
req.rawBody,
req.headers['stripe-signature'] || '',
functions.config().stripe.secret
);
} catch (error) {
resp.status(401).send('Webhook Error: Invalid Secret');
return;
}
const invoice = event.data.object as Stripe.Invoice;
const customerId= invoice.customer as string;
await insertInvoiceRecord(invoice, customerId);
resp.status(200).send(invoice.id);
}
);
/**
* Create an Invoice record in Firestore when a customer's monthly subscription payment succeeds.
*/
const insertInvoiceRecord = async (
invoice: Stripe.Invoice,
customerId: string
): Promise<void> => {
// Invoice is an interface with only fields I care about
const invoiceData: Invoice = {
invoiceId: invoice.id,
...map invoice data here
};
await admin
.firestore()
.collection('invoices')
.doc(invoice.id)
.set(invoiceData);
};
Once deployed, I went to the Stripe developer dashboard (https://dashboard.stripe.com/test/webhooks) and added a new Webhook listening for the Event type invoice.payment_succeeded and with the url being the Firebase function I just made.
NOTE: I had to deploy my Stripe API key and Webhook Secret as enviroment variables for my Firebase function with the following command: firebase functions:config:set stripe.key="sk_test_123" stripe.secret="whsec_456"
Unfortunately it doesn't look like the extension handles the ongoing payment events: https://github.com/stripe/stripe-firebase-extensions/blob/master/firestore-stripe-subscriptions/functions/src/index.ts#L419-L432
You'd either need to modify that code to include likely invoice.payment_succeeded - and then handle appropriately - or write a cloud function to handle that yourself (similar to what's shown there).
I use Firebase auth and realtime database in my Android app. This is the code that I use in Firebase functions to save the user email into the realtime database when they register in the app with email:
'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.initializeUserProfile = functions.auth.user().onCreate(user => {
const userUid = user.uid;
return admin.auth().getUser(userUid).then(userRecord => {
const userProfile = {
email: userRecord.email
};
return admin.database().ref(`/profiles/${userUid}`).set(userProfile);
}).catch(error => {
console.log("Error fetching user data: ", error);
});
});
exports.removeUserProfile = functions.auth.user().onCreate(user => {
const userUid = user.uid;
return admin.database().ref(`/profiles/${userUid}`).remove();
});
When I register an user in the android app (I use the built in registration UI for Firebase), it gives me no error in the Functions logs:
My problem is that although I don't have an error in the log and the user was added to the Firebase Authentication section, the Realtime database doesn't contain the node with the email. The problem is very sporadic. Sometimes it registers it fine into the realtime database, but sometimes it doesn't (like in the log of Jun 25). In the Android app I try to query the database node of the user after registration to display they email and there I get an error (maybe it is an bug in my app, but anyhow, that code up there should be run on server side and the email should be in the Firebase Realtime Database).
What I also don't know is that why do I have those removeUserProfile calls in the log as I didn't remove any user from the Authentication database or from the Realtime database.
Actually, your two Cloud Functions are triggered with exactly the same event, i.e. onCreate(user). So it is normal that they are triggered (almost) simultaneously and that you see the two invocations in the log.
Since you write that "The problem is very sporadic" what is probably happening is that the new record is first created at /profiles/${userUid} by the initializeUserProfile Cloud Function BUT is then removed by the removeUserProfile Cloud Function.
So you should change the trigger of the removeUserProfile Cloud Function to onDelete():
exports.removeUserProfile = functions.auth.user().onDelete((user) => {
const userUid = user.uid;
return admin.database().ref(`/profiles/${userUid}`).remove();.
});
As of firebase-admin#5.11.0 and firebase-functions#1.0.0 firebase-admin no longer takes in an application config when the app initializes.
I had a firestore function that would generate a custom token using firebase-admin’s createCustomToken. Calling that function would generate a credential that I would pass into initializeApp in the credential attribute. How would I go about doing that now?
Do I need to edit process.env.FIREBASE_CONFIG somehow and put the serialized credential there before calling initializeApp?
Based on this issue in Github, it still works.
https://github.com/firebase/firebase-admin-node/issues/224
The following example worked for me:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const serviceAccount = require('./serviceAccountKey.json');
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: 'https://yourapplication.firebaseio.com/'
});
exports.createToken = functions.https.onCall((data, context) => {
const uid = context.auth.uid;
return admin.auth()
.createCustomToken(uid)
.then(customToken => {
console.log(`The customToken is: ${customToken}`);
return {status: 'success', customToken: customToken};
})
.catch(error => {
console.error(`Something happened buddy: ${error}`)
return {status: 'error'};
});
});
Michael Chen's cloud function appears to trigger from a HTTP request from somewhere (an external server?). My employee wrote a cloud function that triggers when the user logs in:
// this watches for any updates to the user document in the User's collection (not subcollections)
exports.userLogin = functions.firestore.document('Users/{userID}').onUpdate((change, context) => {
// save the userID ubtained from the wildcard match, which gets put into context.params
let uid = context.params.userID;
// initialize basic values for custom claims
let trusted = false;
let teaches = [];
// check the Trusted_Users doc
admin.firestore().collection('Users').doc('Trusted_Users').get()
.then(function(doc) {
if (doc.data().UIDs.includes(uid)) {
// if the userID is in the UIDs array of the document, set trusted to true.
trusted = true;
}
// Get docs for each language in our dictionary
admin.firestore().collection('Dictionaries').get()
.then(function(docs) {
// for each of those language docs
docs.forEach(function(doc) {
// check if the userID is included in the trustedUIDs array in the doc
if (doc.data().trustedUIDs.includes(uid)) {
// if it is, we push the 2-letter language abbreviation onto the array of what languages this user teaches
teaches.push(doc.data().shortLanguage);
}
});
// finally, set custom claims as we've parsed
admin.auth().setCustomUserClaims(uid, {'trusted': trusted, 'teaches': teaches}).then(() => {
console.log("custom claims set.");
});
});
});
});
First, we put in a lastLogin property on the user object, which runs Date.now when a user logs in and writes the time to the database location, triggering the cloud function.
Next, we get the userID from the cloud function response context.params.userID.
Two variables are then initialized. We assume that the user is not trusted until proven otherwise. The other variable is an array of subjects the user teaches. In a roles-based data security system, these are the collections that the user is allowed to access.
Next, we access a document listing the userIDs of trusted users. We then check if the recently logged in userID is in this array. If so, we set trusted to true.
Next, we go to the database and traverse a collection Dictionaries whose documents include arrays of trusted userIDs (i.e., users allowed to read and write those documents). If the user is in one or more of these arrays, he or she gets that document added to the teaches property on his or her user data, giving the user access to that document.
Finally, we're ready to run setCustomUserClaims to customize the token claims.
Here's a variation for a Callable Cloud Function, thanks to Thomas's answer
Once the custom claim is set, you can access the field in/from .. say, a firebase storage rule.
For example:
allow write: if request.auth.token.isAppAdmin == true;
With a Callable Cloud Function, as long as the admin.auth().setCustomUserClaims(..) function is returned somewhere along the promise chain, the claim field will be added to the request.auth.token object:
const functions = require('firebase-functions');
exports.setIsAdminClaim = functions.https.onCall((data, context) => {
var uid = context.auth.uid;
return admin.auth().setCustomUserClaims(
uid, {
isAppAdmin: true
}
)
.then(() => {
var msg = 'isAppAdmin custom claim set';
console.log(msg);
return new Promise(function (resolve, reject) {
var resolveObject = {
message : msg
};
resolve(resolveObject);
});
});
});
I have an Android app using Firebase with database, authentication and cloud functions modules. When a user creates a profile through the app and sets a user full name, the value of
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if (user != null) {
// user signed in
String displayName = user.getDisplayName();
// do whatever with displayName...
} else {
// ....
}
is pretty much as expected - whatever the user put in.
I also have a cloud function, which creates a database record for the new user:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.createUserRecord = functions.auth.user().onCreate(function(event) {
const user = event.data;
const userName = user.displayName || 'Anonymous';
const userSignedUp = (new Date()).toJSON();
console.log('A new user signed up: ', user.uid, userName, userSignedUp);
return admin.database().ref('users').push({
name: `${userName}`,
email: `${user.email}`,
ref: `${user.uid}`,
signup: `${userSignedUp}`
});
});
This works mostly OK, except that both the log entry, and the resulting record in the database list Anonymous as the name value.
Can anyone please tell me where I am going wrong with this? Thanks.
Best regards,
~Todor
UPDATE:
Forgot to mention, the user is created with the email/password provider.
In FirebaseUI, creation of a user via email/password is done in two steps: the user is created using the address and password, then the user profile is update with the entered username. This can be seen in the FirebaseUI RegisterEmailFragment.
At this time, Cloud Functions offers only a trigger for user creation, the one you are using, functions.auth.user().onCreate(). There is no trigger for user profile update. Your trigger fires when the user is created. The event data contains the user profile as it exists at that moment, before it has been updated with the user name.
You will need to use some other means to obtain the user name. One option is to use a database event to trigger a function that would retrieve the user data after the profile is updated:
admin.auth().getUser(uid)
.then(function(userRecord) {
// See the UserRecord reference doc for the contents of userRecord.
console.log("Successfully fetched user data:", userRecord.toJSON());
})
.catch(function(error) {
console.log("Error fetching user data:", error);
});
I need to get a user object, specifically the user email, I will have the user id in this format:
simplelogin:6
So I need to write a function something like this:
getUserEmail('simplelogin:6')
Is that possible?
It is possible with Admin SDK
Admin SDK cannot be used on client, only in Firebase Cloud Functions which you can then call from client. You will be provided with these promises: (it's really easy to set a cloud function up.)
admin.auth().getUser(uid)
admin.auth().getUserByEmail(email)
admin.auth().getUserByPhoneNumber(phoneNumber)
See here https://firebase.google.com/docs/auth/admin/manage-users#retrieve_user_data
In short, this is what you are looking for
admin.auth().getUser(data.uid)
.then(userRecord => resolve(userRecord.toJSON().email))
.catch(error => reject({status: 'error', code: 500, error}))
full snippet
In the code below, I first verify that the user who calls this function is authorized to display such sensitive information about anybody by checking if his uid is under the node userRights/admin.
export const getUser = functions.https.onCall((data, context) => {
if (!context.auth) return {status: 'error', code: 401, message: 'Not signed in'}
return new Promise((resolve, reject) => {
// verify user's rights
admin.database().ref('userRights/admin').child(context.auth.uid).once('value', snapshot => {
if (snapshot.val() === true) {
// query user data
admin.auth().getUser(data.uid)
.then(userRecord => {
resolve(userRecord.toJSON()) // WARNING! Filter the json first, it contains password hash!
})
.catch(error => {
console.error('Error fetching user data:', error)
reject({status: 'error', code: 500, error})
})
} else {
reject({status: 'error', code: 403, message: 'Forbidden'})
}
})
})
})
BTW, read about difference between onCall() and onRequest() here.
Current solution as per latest update of Firebase framework:
firebase.auth().currentUser && firebase.auth().currentUser.email
See: https://firebase.google.com/docs/reference/js/firebase.auth.Auth.html#currentuser
Every provider haven't a defined email address, but if user authenticate with email. then it will be a possible way to achieve above solution.
To get the email address of the currently logged in user, use the getAuth function. For email and password / simplelogin you should be able to get the email like this:
ref = new Firebase('https://YourFirebase.firebaseio.com');
email = ref.getAuth().password.email;
In my opinion, the password object is not very aptly named, since it contains the email field.
I believe it is not a Firebase feature to get the email address of just any user by uid. Certainly, this would expose the emails of all users to all users. If you do want this, you will need to save the email of each user to the database, by their uid, at the time of account creation. Other users will then be able to retrieve the email from the database by the uid .
simple get the firebaseauth instance.
i created one default email and password in firebase. this is only for the security so that no one can get used other than who knows or who purchased our product to use our app.
Next step we are providing singup screen for user account creation.
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
String email = user.getEmail();
every time user opens the app, user redirecting to dashboard if current user is not equal to our default email.
below is the code
mAuth = FirebaseAuth.getInstance();
if (mAuth.getCurrentUser() != null){
String EMAIL= mAuth.getCurrentUser().getEmail();
if (!EMAIL.equals("example#gmail.com")){
startActivity(new Intent(LoginActivity.this,MainActivity.class));
finish();
}
}
i Am also searching for the same solution finally i got it.
I had the same problem. Needed to replace email in Firestore by uid in order to not keep emails all around the place. It is possible to call it from a script on your computer using Service Account. You don't need Firebase Functions for this.
First Generate service account and download its json key.
Firebase Console > gear icon > Project settings > Service accounts > Generate a new private key button.
https://console.firebase.google.com/u/0/project/MYPROJECT/settings/serviceaccounts/adminsdk
Then create project, add the key and call the Admin SDK.
npm init
npm install dotenv firebase-admin
Place the json key file from above into .keys directory, keeping the project directory clean of keys files. Also .gitignore the directory.
Write the path of the json key file into .env file like this: GOOGLE_APPLICATION_CREDENTIALS=".keys/MYPROJECT-firebase-adminsdk-asdf-234lkjjfsoi.json". We will user dotenv to load it later.
Write following code into index.js:
const admin = require('firebase-admin');
admin.initializeApp({
credential: admin.credential.applicationDefault(),
});
(async () => {
const email = "admin#example.com";
const auth = admin.auth();
const user = await auth.getUserByEmail(email);
// Or by uid as asked
//const user = await auth.getUser(uid);
console.log(user.uid, user.email);
//const firestore = admin.firestore();
// Here be dragons...
})();
Run as follows node -r dotenv/config index.js
See the docs
Current solution (Xcode 11.0)
Auth.auth().currentUser? ?? "Mail"
Auth.auth().currentUser?.email ?? "User"