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);
});
Related
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.
I want to export all the users inside of Firebase Auth because I want to migrate away from Firebase. I can get the export of the users just fine with the command:
firebase auth:export users.json --format=json --project [my-project]
However, for all of the users that use Sign In with Apple the providerUserInfo is an empty array, so there is currently no way at all to import them into my own database and keep them as functional accounts that can actually login via SIWA again.
When I look at the auth user by adding an onAuthStateChanged listener and logging the auth user to the console, then providerData.uid is Apple's user id, the exact ID that I need to copy to my new database:
onAuthStateChanged(auth, authUser => {
if (authUser) {
const uid = authUser.providerData[0].uid;
if (authUser.providerData[0].providerId === "apple.com") {
console.log(`Apple ID: ${uid}`);
} else {
console.log(`Email address: ${uid}`);
}
}
});
So the value is definitely stored in Firebase Auth, and it's this value that I need to be able to export for all users.
So my question is: how can I fetch the providerUserInfo for such users? Would the accounts:lookup REST endpoint help? Sadly I can't really figure out how that endpoint is supposed to work, what the idToken you need to send is supposed to be.
I found a way to export all the users, including Apple's internal user id, by using the admin SDK:
const fs = require("fs");
const admin = require("firebase-admin");
admin.initializeApp();
const records = {};
function handleUser(userRecord) {
records[userRecord.uid] = userRecord;
}
const listAllUsers = nextPageToken => {
// List batch of users, 1000 at a time.
return admin
.auth()
.listUsers(1000, nextPageToken)
.then(listUsersResult => {
listUsersResult.users.forEach(userRecord => {
handleUser(userRecord);
});
if (listUsersResult.pageToken) {
// List next batch of users.
return listAllUsers(listUsersResult.pageToken);
}
})
.catch(error => {
console.log("Error listing users:", error);
});
};
// Start listing users from the beginning, 1000 at a time.
listAllUsers().then(() => {
const data = JSON.stringify(records);
fs.writeFile("users.json", data, err => {
console.log("JSON data is saved.");
});
});
ProviderUserInfo includes the following data:
{
displayName?: string,
email: string,
phoneNumber?: string,
photoURL?: string,
providerId: string,
uid: string
}
For SIWA (Sign In with Apple) users, their information is anonymized and you must gain explicit consent from the user to be able to collect their personal information. If you had such information you would use updateProfile() to attach it to their user ID at the top of their UserRecord. If there were a ProviderUserInfo entry for Apple, it would consist of:
{
displayName: null, // SIWA does not provide a display name
email: string, // an anonymized email, same as User.email
phoneNumber: null, // SIWA does not provide a phone number
photoURL: null, // SIWA does not provide profile images
providerId: string, // "apple"
uid: string // same as User.localId
}
As the available data is found elsewhere, it is pointless to include in the response and is omitted.
Transferring SIWA users is not a straightforward process. You must follow the steps outlined in the Transferring apps and users to new teams documentation. In short, you use a "Transfer ID Token" with a freshly signed in user's account details, to ask Apple's auth service "Did this user ever sign in for this old client ID?". The returned response will then either say "Yes, their ID with the old client ID was X" or "No, this is a new user". Based on that you migrate their old data across to your new database and authentication ID.
Importantly, after 60 days, you can no longer transfer users from the old service to the new one.
I have an app with a LOGIN page and a REGISTER page. Both pages have a "Sign in with Google" button, as well as a regular login and password input form for those that don't want to sign in with Google. I am also using FireStore to create user profile documents for registered users. When the user also logs in, the app will query the user's profile for use throughout the app. This all works fine.
I noticed that a google user does not need to "register"...he can still click the login button and it will "sign him up" automatically because that's how Google Auth Provider works. However, since he did not "register", he does not yet have a profile. In this case, I had to write some logic so a profile would be created for a Google user. Although this logic works, I just wonder if this is the best way to do this. Are there best practices for handling Google/Social logins for people skipping the traditional "registering" pages? I know most people would probably head to the register page and register, but there will undoubtedly be some people that will skip that and go start to the LOGIN page and sign in via Google that way.
Here's how I'm handling the login page with Google login button:
login.vue
async logInWithGoogle() {
try {
const provider = new this.$fireAuthObj.GoogleAuthProvider()
const userCredentials = await this.$fireAuth.signInWithRedirect(
provider
) ....
Then in my Store (in my case, Vuex state management pattern), I have the following actions:
store.js
First, this onAuthStateChanged observer will notice the new user state and do the following code:
async onAuthStateChangedAction({ commit, dispatch }, { authUser }) {
if (authUser) {
console.log('user committing from onAuthStateChangedAction...')
commit('SET_CURRENT_USER', authUser)
console.log(
'fetchUserProfile action dispatching from onAuthStateChangedAction...'
)
await dispatch('fetchUserProfile', authUser)
} else {
dispatch('logOutUser')
}
}
That onAuthStateChanged observer will fetch the user's profile (and this is the logic I am concerned with...not sure if this is an ideal way to handle user's logging in via Google for first time and bypassing registration:
async fetchUserProfile({ commit }, user) {
try {
const docRef = this.$fireStore.collection('users').doc(user.uid)
const profile = await docRef.get()
if (profile.exists) {
commit('SET_USER_PROFILE', await profile.data())
console.log(
'user profile EXISTS and set from fetchUserProfile action'
)
} else {
console.log('profile does not exist! Creating...')
await docRef.set({
displayName: user.displayName,
email: user.email,
uid: user.uid,
photoUrl: user.photoURL,
providerId: user.providerData[0].providerId,
createdAt: this.$fireStoreObj.FieldValue.serverTimestamp()
})
const p = await docRef.get()
commit('SET_USER_PROFILE', await p.data())
console.log('user profile set')
}
} catch (error) {
console.log('can not fetch profile', error)
}
},
Thanks for any tips or assurances that I am on the right (or wrong) path on handling this. Thank you!
Why not create an empty document with the user's uid and prompt them to "complete their profile"? Until they do so, force redirect them back to the profile page indefinitely.
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();.
});
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"