How to properly get Twitter screenName from firebase auth? - node.js

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.
}

Related

get user displayName in firebase cloud function using context

In my firebase cloud function, I can get the email of each user that makes a call on the client side using:
context.auth.token.email
Is there a way to obtain the user's display name? I tried context.auth.token.name but it still return an undefined property.
The context.auth.token is an object of type DecodedToken which does not contain user display name. You'll have to use Admin SDK and get user by UID/email.
export const functionName = functions.https.onCall(async (data, ctx) => {
const { uid } = ctx.auth?.token
const user = await getAuth().getUser(uid)
console.log(user.displayName)
// ...
})

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

Google Calendar API and Service Account permission error

I'm trying to integrate the Google Calendar API in my app.
So far i've managed to do this:
Created a new project on Cloud Platform
Enabled Calendar API
Added a new service account with role: Owner
Generated jwt.json
Granted domain-wide for that service account
Shared a calendar with that service account (modify rights)
Enabled in the GSuite the option for everyone out of the organisation to modify the events
Now, my code on node.js looks like this:
const { JWT } = require('google-auth-library');
const client = new JWT(
keys.client_email,
null,
keys.private_key,
['https://www.googleapis.com/auth/calendar']
);
const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`;
const rest = await client.request({url});
console.log(rest);
The error I get is:
Sending 500 ("Server Error") response:
Error: Insufficient Permission
Anyone has any ideea? This gets frustrating.
How about this modification?
I think that in your script, the endpoint and/or scope might be not correct.
Pattern 1:
In this pattern, your endpoint of https://dns.googleapis.com/dns/v1/projects/${keys.project_id} is used.
Modified script:
const { JWT } = require("google-auth-library");
const keys = require("###"); // Please set the filename of credential file of the service account.
async function main() {
const calendarId = "ip15lduoirvpitbgc4ppm777ag#group.calendar.google.com";
const client = new JWT(keys.client_email, null, keys.private_key, [
'https://www.googleapis.com/auth/cloud-platform' // <--- Modified
]);
const url = `https://dns.googleapis.com/dns/v1/projects/${keys.project_id}`;
const res = await client.request({ url });
console.log(res.data);
}
main().catch(console.error);
In this case, it is required to enable Cloud DNS API at API console. And it is required to pay. Please be careful with this.
I thought that the reason of your error message of Insufficient Permission might be this.
Pattern 2:
In this pattern, as a sample situation, the event list is retrieved from the calendar shared with the service account. If the calendar can be used with the service account, the event list is returned. By this, I think that you can confirm whether the script works.
Modified script:
const { JWT } = require("google-auth-library");
const keys = require("###"); // Please set the filename of credential file of the service account.
async function main() {
const calendarId = "###"; // Please set the calendar ID.
const client = new JWT(keys.client_email, null, keys.private_key, [
"https://www.googleapis.com/auth/calendar"
]);
const url = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`; // <--- Modified
const res = await client.request({ url });
console.log(res.data);
}
main().catch(console.error);
Note:
This modified script supposes that you are using google-auth-library-nodejs of the latest version.
Reference:
JSON Web Tokens in google-auth-library-nodejs

Firebase auth email - prevent fake/invalid accounts

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)
})
})

Why do users have different ID formats between the Microsoft Graph API and UserIDs in bot framework?

I have used Graph Service to get user information by email. Here is my sample code:
var user = null;
const GraphService = require('graph-service');
const ClientCredentials = require('client-credentials');
const tenant = 'my-company.com';
const clientId = '0b13aa29-ca6b-42e8-a083-89e5bccdf141';
const clientSecret = 'lsl2isRe99Flsj32elwe89234ljhasd8239jsad2sl=';
const credentials = new ClientCredentials(tenant, clientId, clientSecret);
const service = new GraphService(credentials);
service.get('/users/tnguyen482#my-company.com').then(response => {
user = response.data;
});
This would return user which has ID = 9422e847-0000-1111-2222-d39d550a4fb6
But when I use Botbuilder-teams to get fetch members, the user information return from which has different format of ID. Here is my sample code:
var user = null;
var teams = require("botbuilder-teams");
var connector = new teams.TeamsChatConnector({
appId: process.env.MICROSOFT_APP_ID,
appPassword: process.env.MICROSOFT_APP_PASSWORD
});
let conversationId = session.message.address.conversation.id;
var userEmail = "tnguyen482#my-company.com";
connector.connector.fetchMembers(
"https://smba.trafficmanager.net/amer-client-ss.msg/",
conversationId,
(err, result) => {
if (err) {
console.log('Cannot get member of current conversation');
}
else {
if (result.length > 0){
result.forEach(function(item) {
if (item.email == userEmail){
user = item;
}
});
}
}
}
);
This would return user which has ID = 29:1zJXjlM7ifjqawGVxXx_4xxx56BFCCIJWfPbWrVDSdxsKUhi9IXyXXYNLOKCLHodN7WgEzz31lBKcZwtWvMzoUw
My question is why on the same user with different ways approach to retrieve data return different ID format?
Besides, my purpose is that I will use the user ID in address for botbuilder to send personal message to user.
User ID is not defined the same within the Graphs Service as it is within Botbuilder. The botbuilder userID is a key for that given user as connected to the conversation within the bot (and is only relevant within the context of the conversation with the bot), and the userID within Graph Service is a unique identity key for a user of Azure AD.
These are not the same API or part of a universal connector, so these IDs do not cross over to one another. Many people create some sort of dictionary of users so that the 2 can be looked up and used accordingly in their application.

Resources