I have a Firebase Cloud Function that does the following:
const firestore = require('firebase-admin')
const functions = require('firebase-functions')
const regex = require('../../utils/regex')
exports = module.exports = functions.https.onCall((data, context) => {
if (!context.auth) {
throw new functions.https.HttpsError('failed-precondition', 'The function must be called while authenticated.')
}
if (!data['displayName']) {
throw new functions.https.HttpsError('failed-precondition', 'An display name must be provided.')
}
if (!regex.noSpecialCharactersExceptSpace.test(data['displayName'])) {
throw new functions.https.HttpsError('input-validation-failed', 'Your display name cannot have special characters in it.')
}
return firestore.firestore().collection('profiles').doc(context.auth.uid)
.update({
displayName: data['displayName']
})
.then(() => {
console.info('Successful public profile update user='+ context.auth.uid)
return { text: 'Your profile has successfully been updated' };
})
.catch((error) => {
console.error('Error updating public profile user=' + context.auth.uid + ' error=' + error)
throw new functions.https.HttpsError('failed-precondition', 'An error happened, our team will look into it.')
})
})
From my front-end, when I call this function, it can take up to 20-30 seconds for it to complete and return a status code. This delay really disrupts the user experience.
What's the best way to improve the response time?
Testing
Direct calls to Firestore from the UI resolve incredibly quickly. Thus, it seems unlikely that the problem is DNS from our side.
Other API calls to Cloud Function from the UI, like "Invite a Friend", resolve quickly, and are not affected by this fault.
Calls to other Cloud Functions, which do not return a Promise, but do things like send emails with Postmark, are not affected by this fault and also resolve incredibly quickly. Thus, it seems that the problem isn't the location of the Firebase project (us-central1). Although changing the location hasn't been tested.
The fault only occurs on this one function. The one copied and pasted above.
The only difference between the affected function, and our other functions, is that the one with the fault returns a Promise for a Firestore operation.
Related
I read all the answers I found about this but I still have some problems. Probably something with .ref(), but I can't see what I'm doing wrong. My cloud function is not triggered at all.
DB example:
business/{businessId}/reservations/{reservationId}
I want to trigger this function every time when a new reservation is created [a new document is created in reservation collection] (business/{businessId}/reservations/).
And then I want to sent a notification message, but that is another thing.
const functions = require('firebase-functions');
const admin = require('firebase-admin');
exports.sendAdminNotification = functions.database
.ref("business/{businessId}/reservations")
.onWrite((event: any) => {
// It never comes here...
console.log('Here');
const payload = {
notification: {
title: 'New registration',
body: 'You have new registration!',
},
};
// You can ignore this part
admin
.messaging()
.sendToDevice('SomeToken', payload)
.catch(function (error: any) {
console.log('Notification sent failed:', error);
});
});
The fact that you're using the terms "document" and "collection" suggests that your database is Firestore. But what you've written here is a Realtime Database trigger. Realtime Database is a completely different database. Instead, you will need to write a Firestore trigger instead. They begin with functions.firestore.
I am using passwordless authentication for a project, everything is working as expected, however I have one question about this authentication. I will talk about the scenario.
First step: as we all know, a new user needs an email and then proceeds to click the link to login.
That is the normal case, no problem with it, but what if a user has already done that step and say he/she logs out from the app? it seems like they need to do the first step I described above again.
Here is what I have tried so far:
login() {
const email = this.email;
this.$store
.dispatch("LOGIN", { email })
.then(resp => {
this.$router.replace("/");
})
.catch(err => {
this.autherror = true,
this.errorMessage = err.message;
});
}
LOGIN: ({ commit }, user) => {
return new Promise((resolve, reject) => {
// commit(AUTH_REQUEST)
firebase.auth().signInWithEmailLink(user.email, window.location.href)
.then((result) => {
window.localStorage.removeItem("emailForSignIn");
resolve(result);
})
.catch((err) => {
reject(err);
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
});
},
I will get an error "Invalid email link!" trying the above code and even if I put the url as the previous one I logged in with, It will also throw an error "The action code is invalid. This can happen if the code is malformed, expired, or has already been used"
I can understand the point why an email to login is always required but the main point am trying to say is, if a user log's in from the link at first and then log's out, they can sign in the app without needing to do first step again, how? that means if there is a way to store credentials in cookies/localstorage, and the only time they need to do the first step again is if they clear the cookies, storage etc. from all or that particular app/page requiring.
So is it possible? It is something that will definitely improve user experience.
You should read and understand how users work in Firebase (and basically the same in any oAuth type verification system) - https://firebase.google.com/docs/auth/users
and more particularly, how email is used - https://firebase.google.com/docs/auth/web/email-link-auth
In your code you should use the email confirmation steps as shown in the reference above (so, something like the code below - you may need some minor changes to fit your local scenario):
LOGIN: ({ commit }, user) => {
return new Promise((resolve, reject) => {
// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
// Additional state parameters can also be passed via URL.
// This can be used to continue the user's intended action before triggering
// the sign-in operation.
// Get the email if available. This should be available if the user completes
// the flow on the same device where they started it.
var email = window.localStorage.getItem('emailForSignIn');
if (!email) {
// User opened the link on a different device. To prevent session fixation
// attacks, ask the user to provide the associated email again. For example:
email = window.prompt('Please provide your email for confirmation');
}
// commit(AUTH_REQUEST)
firebase.auth().signInWithEmailLink(email, window.location.href)
.then((result) => {
window.localStorage.removeItem("emailForSignIn");
resolve(result);
})
.catch((err) => {
reject(err);
// Some error occurred, you can inspect the code: error.code
// Common errors could be invalid email and invalid or expired OTPs.
});
});
}
},
Just indicate in your db that the person has been verified in case you don't want the local storage to store the data
I am attempting to add MFA for user authentication to an already existing solution (built in Angular) for device management within AWS Cognito.
I am having trouble figuring out how to handle this particular response well from a user-experience perspective. It actually feels broken, so would love if anyone else has experience pain points here.
See Use Case 23. for example implementation, mine is below:
authenticate(username: string, password: string): Observable<any> {
// init cognitoUser here
return new Observable((observer) => {
cognitoUser.authenticateUser(authenticationDetails, {
onSuccess: (result: any) => {},
onFailure: (err: Error) => {},
mfaRequired: (codeDeliveryDetails: any) => {
// SMS has just been sent automatically
// and it needs to be confirmed within this scope
// The example linked requests the code via `confirm()`
// which is awful UX...and since this is a service
// probably non-compliant with best practice
// However, without this `confirm` at this point in
// time, we have no confirmationCode below
cognitoUser.sendMFACode(confirmationCode, {
onSuccess: (result) => {
observer.next(result);
observer.complete();
}, onFailure: (err: Error) => {
observer.error(err);
observer.complete();
}
});
}
});
});
}
Expected:
If the user authenticates successfully but has not added this device through MFA, we can manage the redirect to appropriate confirmation code form page and trigger the sendMFACode function manually (perhaps through some sort of limited session?)
Issue/s:
we don't have a session, so we have no way of asking the user the MFA code sent automatically outside of this login screen...catch 22?
adding another show/hide field in the login form doesn't work as it would hit the sendMfaCode function multiple times, resulting in multiple SMS codes sent.
Has anyone had any luck stepping out of this flow?
Whilst I’m sure very talented people worked on the amazon-cognito-identity-js API, it is just straight up badly designed. Thus why it’s been depricated. My personal advise would be to migrate to Amplify, which makes me much less angry.
With Amplify you can do these ones.
import Amplify from 'aws-amplify'
import Auth from '#aws-amplify/auth'
let mfaRequired = false
Amplify.configure({
Auth: {
userPoolWebClientId: '',
userPoolId: ''
}
})
const logUserIn = (user) => {
// Go forth and be happy
}
// Run me on your login form's submit event
const login = async (username, password) => {
const user = await Auth.signIn(username, password)
if (user.challengeName === 'SMS_MFA') {
// Change UI to show MFA Code input
mfaRequired = true
return
}
return logUserIn(user)
}
// Run me when the user submits theire MFA code
const senfMfaCode = async (mfaCode) => {
const user = await Auth.confirmSignIn(mfaCode)
return logUserIn(user)
}
BUT if for some sad reason you need to keep using amazon-cognito-identity-js don’t worry. I got you.
Just keep the cognitoUser object stored outside the callback. The documentation is a little misleading because it only show’s self contained examples but there’s no reason that you can’t notify your UI when MFA is required and then call cognitoUser.sendMFACode() later.
Just remember that the documentation show’s the passing of this to sendMFACode() for scoping (which is terrible) but you can just declare your callbacks as a variable and share it between your authenticateUser() and sendMFACode() functions (or as many functions as you like).
import { CognitoUserPool, AuthenticationDetails, CognitoUser } from 'amazon-cognito-identity-js'
export let mfaRequired = false
export let cognitoUser = null
export const cognitoCallbacks = {
mfaRequired () {
// Implement you functionality to show UI for MFA form
mfaRequired = true
},
onSuccess (response) {
// Dance for joy the code gods be glorious.
},
onFailure () {
// Cry.
}
}
export const logUserIn = payload => {
cognitoUser = new CognitoUser({
Username: 'Matt Damon',
Pool: new CognitoUserPool({
UserPoolId: '',
ClientId: ''
})
})
return cognitoUser.authenticateUser(new AuthenticationDetails(payload), cognitoCallbacks)
}
export const sendMfaCode = MFACode => {
cognitoUser.sendMFACode(MFACode, cognitoCallbacks)
}
That’s a super basic implementation and on top of that you could,
Just overwrite the mfaRequired function in an external module to do whatever you want.
Wrap the whole thing in a pub/sub plugin and subscribe to events.
Hope that helps!
I know this is an old question, but I thought this answer might be helpful for anyone who is still using the amazon-cognito-identity-js API instead of Amplify. #stwilz's answer works somewhat, but there are a few complications that come when you stray too far away from the documentation's use cases (and might come about when doing TOTP MFA instead of SMS MFA). I've created a workaround to address situations where you might get errors like Invalid Access Token, Missing parameter Session, or Invalid session for the user.
If you need to use something like sendMFACodeoutside of the callbacks, it's not enough to just keep cognitoUser stored outside the callback. You actually have to call the authenticateUser function again, then call the sendMFACode within the callback. It gets more complicated with verifySoftwareToken for TOTP, where you actually have to store the Cognito user object and then reassign it when calling authenticateUser again.
If none of this makes sense, I've created a simple Github Gist that uses React and amazon-cognito-identity-js to show how such a flow would work. It's here: https://gist.github.com/harve27/807597824720d0919476c0262e30f587
I apologize if I am posting too many questions regarding Cloud Functions on this forum. I am excited about the project and in the process of moving out my firebase-queue workers which I am using in my app, which is also currently in Beta and would be generally available soon, to Cloud Functions one by one and I want to make sure that I am not missing anything. I have a cloud function which is used to generate an authentication token which is then stored on the database again which is then read by the app to authenticate the user. The function is pretty simple and doesn't contain any work which may take varying amount of time to finish.
exports.generateAuthenticationToken = functions.database.ref('/tasks/authentication/{taskId}')
.onWrite(event => {
const taskSnapshot = event.data
if(!taskSnapshot.exists()) {
return
}
const task = taskSnapshot.val()
const uid = task.uid
// Make sure that the user is an app user
return ref.child('users').child(uid).once('value').then((snapshot) => {
if(!snapshot.exists()) {
console.error('Unable to authenticate user: Invalid UID: ', uid)
return ref.child('taskResults/' + event.params.taskId).set({
category: 'authentication',
status: 'failure',
error: 'Invalid uid',
})
} else {
// Generate a token using
return admin.auth().createCustomToken(uid).then((token) => {
return ref.child('taskResults/' + event.params.taskId).set({
category: 'authentication',
status: 'success',
result: {
authToken: token
}
})
})
}
}).then(() => {
ref.child('/tasks/authentication/' + event.params.taskId).remove()
})
})
On the Firebase Functions console, in the Logs section, I see logs that some invocations of this functions finish in less than 10 ms and some take as long as 4000 ms. I wanted to ask under what circumstances would the run time for a cloud function doing constant kind of vary so much. This particular worker is kind of critical for my app because the user has to wait to be authenticated to be able to use the app and 4 secs + the time taken to read back the token and authenticate makes it too long.
I'm exploring the firebase cloud functions and I'm trying to send a notifications with an http request.
The problem is that even if I manage to send the notification, the request always goes timeout.
Here's my script
/functions/index.js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp(functions.config().firebase);
exports.friendRequestNotification = functions.https.onRequest((req, res) => {
const senderId = req.query.senderId;
const recipientId = req.query.recipientId;
const getRecipientPromise = admin.database().ref(`/players/${recipientId}`).once('value');
const getSenderPromise = admin.database().ref(`/players/${senderId}`).once('value');
return Promise.all([getRecipientPromise, getSenderPromise]).then(results => {
const recipient = results[0];
const sender = results[1];
const recipientToken = recipient.child("notificationsInfo/fcmToken").val();
const notificationAuthorization = recipient.child("notificationsInfo/wantsToReceiveNotifications").val();
const recipientBadge = recipient.child("notificationsInfo/badgeNumber").val();
const senderUsername = sender.child("username").val();
const payload = {
notification: {
title: `FriendRequest`,
body: `You have a new friend request from ${senderUsername}!`,
badge: (recipientBadge+1).toString()
}
};
if (notificationAuthorization) {
return admin.messaging().sendToDevice(recipientToken, payload).then(response => {
});
}
return admin.database().ref(`/players/${recipientId}/notificationsInfo/badgeNumber`).setValue(recipientBadge+1);
});
});
Plus It seems that the badgeNumber in never updated, is that related to the timeout issue?
HTTP-triggered Cloud Functions work just like Express apps -- you have a response object (res) that you need to use to send something when the request is done. In this case, it looks like you could do something like:
return Promise.all([
/* ... */
]).then(() => {
res.status(200).send('ok');
}).catch(err => {
console.log(err.stack);
res.status(500).send('error');
});
#Michael Bleigh answer is perfectly fine for this question, let me add more in this for the future users.
As per firebase documentation:-
Use these recommended approaches to manage the lifecycle of your
functions:
Resolve functions that perform asynchronous processing (also known as
"background functions") by returning a JavaScript promise.
Terminate HTTP functions with res.redirect(), res.send(), or res.end(). (The case in this question.)
Terminate a synchronous function with a return; statement.
Note
It's important to manage the lifecycle of a function to ensure that it resolves properly. By terminating functions correctly, you can avoid excessive charges from functions that run for too long or loop infinitely. Also, you can make sure that the Cloud Functions instance running your function does not shut down before your function successfully reaches its terminating condition or state.
You need a paid plan (Blaze, pay as you go) to access external APIs.
You might see below warning in firebase functions log if the billing account is not configured.
Billing account not configured. External network is not accessible and
quotas are severely limited. Configure billing account to remove these
restrictions
Check this link for more information.