Firebase auth email - prevent fake/invalid accounts - security

I am using Firebase auth email accounts to sign up users to a site.
What I have noticed lately is the below cases.
Users sign up using a valid email address and then never verify the
email address
Users attempt to sign up using a fake email address
For the first case we can search all accounts that have not been verified within a time span and delete them.
admin.auth().getUser(uid).then(user => {
const creationTime = user.metadata.creationTime
const isVerified = user.emailVerified
const lastSignInTime = user.lastSignInTime
if(!isVerified){
// Process and delete unverified accounts after x days
...
}
})
How can we handle accounts where the email address is fake or misspelled? I am not seeing any property on the firebase.User object that indicates an invalid email address. We do however receive a mail delivery failure message for each user that has signed up using a invalid email address - this is not enough to automatically purge fake / invalid accounts.
What are best practices on preventing fake signups?
Kind regards /K

You can't stop someone from using any string that looks like an email address, and the system doesn't have a way of telling you that the verification email was successfully sent.
The usual way to deal with this is to create some database record for each user account that tracks their validation status. You can query the database to find out which users have not validated after some amount of time. Your app should be sending your backend ID tokens from the user that can be used to check if they are validated, and if so, update the database to show that it happened.

So this is the code I came up with to purge unverified accounts.
May not be the most elegant solution, but works.
exports.scheduledUserCleanup = functions
.region('europe-west1')
.pubsub
.schedule('0 3 * * *')
.timeZone('Europe/Stockholm')
.onRun(async (event) => {
const today = moment()
const inactivityThresholdDays = 7 //Get purge threshold days
let myPromises = [] //Collect all promises to carry out
//Search for users that have NOT validated email
database.ref('user-signups').once('value', (usersSnapshots) => {
usersSnapshots.forEach((snapshot) => {
const uid = snapshot.key
// Get user from firebase auth
admin.auth().getUser(uid)
.then((firebaseAuthUser) => {
const creationTimeStr = firebaseAuthUser.metadata.creationTime
const isVerified = firebaseAuthUser.emailVerified
const lastSignInTimeStr = firebaseAuthUser.metadata.lastSignInTime
const neverSignedIn = (creationTimeStr === lastSignInTimeStr) ? true : false
if(!isVerified && neverSignedIn){
// Process and delete unverified accounts after 7 days
const creationTime = moment(creationTimeStr)
const daysSinceCreation = today.diff(creationTime, 'days')
if(daysSinceCreation > inactivityThresholdDays){
console.log('Remove user from db and Firebase auth', uid)
myPromises.push( admin.auth().deleteUser(firebaseAuthUser.uid) )
myPromises.push( database.ref(`user-signups/${uid}`).remove() )
}else{
console.log(`Keep for ${inactivityThresholdDays} days before deleting`, uid)
}
}
return true
})
.catch((error) => {
// Remove if not found in Firebase Auth
const notFoundInFirebaseAuth = (error.code) ? error.code === 'auth/user-not-found' : false
if(notFoundInFirebaseAuth){
console.log('Remove user from db', uid)
myPromises.push( database.ref(`user-signups/${uid}`).remove() )
}
return false
})
})
})
// Execute promises
return Promise.all(myPromises)
.then(() => Promise.resolve(true))
.catch((err) => {
console.error('Error', err)
return Promise.reject(err)
})
})

Related

Using wildcards in firestore get query

I want to create a cloud function in firebase that gets triggered whenever a user logs in for the first time. The function needs to add the UID from the authentication of the specific user to a specific, already existing document in firestore. The problem is that the UID needs to be added to a document of which I do not know the location. The code I have right now doesn't completely do that, but this is the part where it goes wrong. The database looks like this when simplified
organisations
[randomly generated id]
people
[randomly generated id] (in here, a specific document needs to be found based on known email
adress)
There are multiple different organisations and it is unknown to which organisation the user belongs. I thought of using a wildcard, something like the following:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
console.log('function ready');
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
//Get email, either personal or work
console.log('Taking a snapshot...');
const snapshot = db.collection('organisations/{orgID}/people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log(doc.data());
});
});
}
I commented out some authentication-based lines for testing purposes. I know the code still runs, because hardcoding the orgID does return the right values. Also, looping trough every organisation is not an option, because I need to have the possibility of having a lot of organisations.
A lot of solutions are based on firestore triggers, like onWrite, where you can use wildcards like this.
However, I don't think that's possible in this case
The solution to the problem above:
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const db = admin.firestore();
//Add UID to document in DB[FMIS-94]
//Detect first login from user
//if(firebase.auth.UserCredential.isNewUser()){
if(true){
//User is logged in for the first time
//const userID = firebase.auth().currentUser.UID;
//const userEmail = firebase.auth().currentUser.email;
const userID = '1234567890';
const userEmail = 'example#example.com';
var docFound = false;
//Get email, either personal or work
console.log('Taking a snapshot...');
//Test for work email
const snapshot = db.collectionGroup('people').where('email.work', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//work email found
console.log('work email found');
console.log(doc.data());
docFound = true;
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
if(!docFound){
//Test for personal email
const snapshot = db.collectionGroup('people').where('email.personal', '==', userEmail).get()
.then(function(querySnapshot){
querySnapshot.forEach(function(doc){
//personal email found
console.log('personal email found');
console.log(doc.data());
const organisationID = doc.ref.parent.parent.id;
writeUID(doc.id, userID, organisationID);
});
});
}
}
async function writeUID(doc, uid, organisationID){
const res = db.collection(`organisations/${organisationID}/people`).doc(doc).set({
userId: uid
}, { merge: true });
}
This was exactly what I needed, thanks for all your help everyone!
It is not possible to trigger a Cloud Function when a user logs in to your frontend application. There is no such trigger among the Firebase Authentication triggers.
If you want to update a document based on some characteristics of the user (uid or email), you can do that from the app, after the user has logged in.
You mention, in your question, "in here, a specific document needs to be found based on known email address". You should first build a query to find this document and then update it, all of that from the app.
Another classical approach is to create, for each user, a specific document which uses the user uid as document ID, for example in a users collection. It is then very easy to identify/find this document, since, as soon the user is logged in you know his uid.
I'm not sure I understand you correctly, but if you want to search across all people collections not matter what organizations document they're under, the solution is to use a collection group query for that.
db.collectionGroup('people').get()
.then(function(querySnapshot) {
querySnapshot.forEach(function(doc) {
console.log("user: "+doc.id+" in organization: "+doc.ref.parent.parent.id);
});
});
This will return a snapshot across all people collections in your entire Firestore database.
First setup Cloud Functions according to the official Documentation.
Then after setting up create functions like this:
exports.YOURFUNCTIONNAME= functions.firestore
.document('organisations/[randomly generated id]/people/[randomly generated id]')
.oncreate(res => {
const data = res.data();
const email = data.email;/----Your field name goes here-----/
/-----------------Then apply your logic here---------/
)}
This will triggers the function whenever you create the People -> Random ID

Xero API 403 - AuthenticationUnsuccessful error

I am using the Xero Api with Nodejs and the xero-node library.
I have completed the oAuth flow and saved the token to the database. The issue i am now having is continually getting a 403 forbidden error when attempting to get anything from Xero be that Contacts, Accounts or Users. Sample code is below
I can get tenants ok without an issue however anything else doesn't work. I have checked the scopes to make sure when I am setting up the client they are correct which they are.
var getStuff = async(tokenSet) => {
await xero.setTokenSet(tokenSet);
const tenants = await xero.updateTenants();
const xeroTenantId = tenants[0].id // {String} Xero identifier for Tenant
const ifModifiedSince = new Date("2020-02-06T12:17:43.202-08:00");
const where = 'IsSubscriber==true'; // {String} Filter by an any element
const order = 'LastName ASC'; // {String} Order by an any element
console.log(tenants);
try {
const response = await xero.accountingApi.getUsers(xeroTenantId, ifModifiedSince, where, order);
console.log(response.body || response.response.statusCode)
}
catch (err) {
/// console.log(err);
console.log(`There was an ERROR! \n Status Code: ${err.response.statusCode}.`);
console.log(`ERROR: \n ${JSON.stringify(err.response.body, null, 2)}`);
}
}
Which scopes have been added to the access token you are passing through? You can decode your token here https://jwt.io/
Also - you need to pass the ‘tenant.tenantId’ to the function. I believe the tenant.id actually relates to the connection id which is coming back from the /connections endpoint.
My hunch is that is the issue. Also curious how that’s working, as updateTenants() should have the empty function call on the end. Is that working for you?

firebase-admin: Get token of failed notification delivery result

Let's say we want to send a notification to two registrationTokens (only Android devices, no iOS) like this:
const tokens = ['tokenA', 'tokenB'];
const payload = {badge: 1, title: 'Hello', body: 'world'};
const options = {priority: 'high',contentAvailable: false, timeToLive: 60 * 60 * 24};
const admin = FirebaseAdmin.initializeApp({/*config here...*/});
admin.messaging().sendToDevice(deviceTokens, payload, options)
.then((response) => {
response.results.forEach((deviceResult) => {
if (deviceResult.error) {
console.log('Delivery failed. Showing result:\n', deviceResult);
}
});
});
The user who's device once registered with tokenB deleted the app from his device. Therefore the token is not registered anymore with firebase.
The error object looks like this then:
Delivery failed. Showing result:
{"error":
{
"code":"messaging/registration-token-not-registered",
"message":"The provided registration token is not registered. A previously valid registration token can be unregistered for a variety of reasons. See the error documentation for more details. Remove this registration token and stop using it to send messages."
}
}
The Problem:
My problem is, that I only know that one of the deliveries failed. But I don't know to which token the error is related. Therefore I cannot remove the outdated token from the database.
The Question:
Is there a way to find out for which tokens the deliveries have failed?
Github Issue Link: https://github.com/firebase/firebase-admin-node/issues/600
You need to use the index in forEach and get the token from your array you passed in sendToDevice.
Official docs: https://firebase.google.com/docs/reference/admin/node/admin.messaging.MessagingDevicesResponse
This seems like a hack but it works for me when I have multiple device tokens of a single user as I have to store new one whenever they login.
const tokens = ['tokenA', 'tokenB'];
const payload = {badge: 1, title: 'Hello', body: 'world'};
const options = {priority: 'high',contentAvailable: false, timeToLive: 60 * 60 * 24};
const admin = FirebaseAdmin.initializeApp({/*config here...*/});
admin.messaging().sendToDevice(deviceTokens, payload, options)
.then((response) => {
response.results.forEach((deviceResult,index) => {
if (deviceResult.error) {
let failedToken = tokens[index];
// Now use this token to delete it from your DB, or mark it failed according to your requirements.
}
});
});
This method is also used in firbease samples as well: https://github.com/firebase/functions-samples/blob/master/fcm-notifications/functions/index.js

Access Facebook Messenger User Profile API in DialogFlow

I'm building a cross-platform chatbot in Google's DialogFlow. I'd like to access the Facebook User Profile API to learn the user's first name.
I'm struggling to find advice on how (or if) I can make this happen.
https://developers.facebook.com/docs/messenger-platform/identity/user-profile/
Has anybody here achieved this?
I did that for one of my bots yesterday, you need 2 things, first the Page Token and second is the psid which is Page scope user ID.
On dialogflow, you will receive the request block with psid as sender id. You can find it at:
agent.originalRequest.payload.data.sender.id
This psid needs to be passed to api get request at
/$psid?fields=first_name with your page Token as accessToken to get the first name in response.
You need to make a call to Facebook Graph API in order to get user's profile.
Facebook offers some SDKs for this, but their official JavaScript SDK is more intended to be on a web client, not on a server. They mention some 3rd party Node.js libraries on that link. I'm particularly using fbgraph (at the time of writing, it's the only one that seems to be "kind of" maintained).
So, you need a Page Token to make the calls. While developing, you can get one from here:
https://developers.facebook.com/apps/<your app id>/messenger/settings/
Here's some example code:
const { promisify } = require('util');
let graph = require('fbgraph'); // facebook graph library
const fbGraph = {
get: promisify(graph.get)
}
graph.setAccessToken(FACEBOOK_PAGE_TOKEN); // <--- your facebook page token
graph.setVersion("3.2");
// gets profile from facebook
// user must have initiated contact for sender id to be available
// returns: facebook profile object, if any
async function getFacebookProfile(agent) {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
let payload;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
if ( fbSenderID ) {
try { payload = await fbGraph.get(fbSenderID) }
catch (err) { console.warn( err ) }
}
return payload;
}
Notice you don't always have access to the sender id, and in case you do, you don't always have access to the profile. For some fields like email, you need to request special permissions. Regular fields like name and profile picture are usually available if the user is the one who initiates the conversation. More info here.
Hope it helps.
Edit
Promise instead of async:
function getFacebookProfile(agent) {
return new Promise( (resolve, reject) => {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
fbGraph.get( fbSenderID )
.then( payload => {
console.log('all fine: ' + payload);
resolve( payload );
})
.catch( err => {
console.warn( err );
reject( err );
});
});
}

How to properly get Twitter screenName from firebase auth?

Following these links https://firebase.google.com/docs/auth/web/manage-users#update_a_users_profile and https://firebase.google.com/docs/auth/web/manage-users#get_a_users_provider-specific_profile_information
I was able to authenticate user and log in with their twitter account. However, I want to get the screenName of an user. How will I do that?
I've checked some network request and I see the screenName attribute. I've checked onAuthStateChanged and it returns user attribute but without the screenName.
I needed to do this from Node, however, Google/Firebase Auth does not store a user's Twitter handle (at least it's not accessible through firebase-admin).
However, they do make the Twitter uid accessible as the question points out. With that, you can subsequently call Twitter's API to get a user by their uid and the result will return the handle a.k.a username:
import { TwitterApi } from 'twitter-api-v2';
import { auth } from 'firebase-admin';
const BEARER = process.env['TWITTER_BEARER_TOKEN'] as string;
const logTwitterHandleFromUid = async (googleUid: string): Promise<void> => {
// get Google Auth information associated with the target user
const { providerData } = await auth().getUser(googleUid);
// pull out the Twitter information
const twitter = providerData.find((p) => p.providerId.includes('twitter'));
if (!twitter) {
console.error('User does not have a linked Twitter account')
process.exit(1)
}
const details = await new TwitterApi(BEARER).v2.user(twitter.uid);
// pull out the Twitter handle
const twitter_handle = details.data.username;
return console.log(twitter_handle);
};
See Get a user's profile documentation. The .displayName property should have it.
var user = firebase.auth().currentUser;
var name, email, photoUrl, uid, emailVerified;
if (user != null) {
name = user.displayName;
email = user.email;
photoUrl = user.photoURL;
emailVerified = user.emailVerified;
uid = user.uid; // The user's ID, unique to the Firebase project. Do NOT use
// this value to authenticate with your backend server, if
// you have one. Use User.getToken() instead.
}

Resources