How to complete login only after functions.auth.user().onCreate is finished - firebase

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)

AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;

Related

signInWithEmailAndPassword: getting auth/user-token-expired [duplicate]

I am using Firebase authentication in my iOS app. Is there any way in Firebase when user login my app with Firebase then logout that user all other devices(sessions)? Can I do that with Firebase admin SDK?
When i had this issue i resolved it with cloud functions
Please visit this link for more details https://firebase.google.com/docs/auth/admin/manage-sessions#revoke_refresh_tokens
Do the following;
Set up web server with firebase cloud functions (if none exists)
use the admin sdk(thats the only way this method would work) - [Visit this link] (
(https://firebase.google.com/docs/admin/setup#initialize_the_sdk).
Create an api that receives the uid and revokes current sessions as specified in the first link above
admin.auth().revokeRefreshTokens(uid)
.then(() => {
return admin.auth().getUser(uid);
})
.then((userRecord) => {
return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
})
.then((timestamp) => {
//return valid response to ios app to continue the user's login process
});
Voila users logged out. I hope this gives insight into resolving the issue
Firebase doesn't provide such feature. You need to manage it yourself.
Here is the Firebase Doc and they haven't mentioned anything related to single user sign in.
Here is what you can do for this-
Take one token in User node (Where you save user's other data) in Firebase database and regenerate it every time you logged in into application, Match this token with already logged in user's token (Which is saved locally) in appDidBecomeActive and appDidFinishLaunching or possibly each time you perform any operation with Firebase or may be in some fixed time interval. If tokens are different logged out the user manually and take user to authenticate screen.
What i have done is:
Created collection in firestore called "activeSessions".User email as an id for object and "activeID" field for holding most recent session id.
in sign in page code:
Generating id for a user session every time user is logging in.
Add this id to localstorage(should be cleaned everytime before adding).
Replace "activeID" by generated id in collection "activeSessions" with current user email.
function addToActiveSession() {
var sesID = gen();
var db = firebase.firestore();
localStorage.setItem('userID', sesID);
db.collection("activeSessions").doc(firebase.auth().currentUser.email).set({
activeID: sesID
}).catch(function (error) {
console.error("Error writing document: ", error);
});
}
function gen() {
var buf = new Uint8Array(1);
window.crypto.getRandomValues(buf);
return buf[0];
}
function signin(){
firebase.auth().signInWithEmailAndPassword(email, password).then(function (user) {
localStorage.clear();
addToActiveSession();
}
}), function (error) {
// Handle Errors here.
var errorCode = error.code;
var errorMessage = error.message;
if (errorCode === 'auth/wrong-password') {
alert('wrong pass');
} else {
alert(errorMessage);
}
console.log(error);
};
}
Then i am checking on each page if the id session in local storage is the same as "activeID" in firestore,if not then log out.
function checkSession(){
var db = firebase.firestore();
var docRef = db.collection("activeSessions").doc(firebase.auth().currentUser.email);
docRef.get().then(function (doc) {
alert(doc.data().activeID);
alert(localStorage.getItem('userID'));
if (doc.data().activeID != localStorage.getItem('userID')) {
alert("bie bie");
firebase.auth().signOut().then(() => {
window.location.href = "signin.html";
}).catch((error) => {
// An error happened.
});
window.location.href = "accountone.html";
} else{alert("vse ok");}
}).catch(function (error) {
console.log("Error getting document:", error);
});
}
PS: window has to be refreshed to log inactive session out.

How do I link auth users to collection in Firestore?

I'm trying to connect a user to the user collection in firestore. I'm using cloud functions, but I don't think I'm implementing it correctly.
firebase.auth().createUserWithEmailAndPassword(email, password)
.then(() => {
console.log('user created')
exports.createUserDoc = functions.auth.user().onCreate((user) => {
console.log("hi")
const userId = user.uid;
const account = {
posts: []
}
return admin.firestore().collection("Users").doc(userId).add(account)
})
But my console.log(hi) isn't showing up. Am I approaching this correctly? Any advice helps!
Right now what i have done is when a user creates an account
i will log the login information into the database.
The document name is set to the user UID that firebase give the user.
Now you can simply request the data from the database with the user UID as
being your .doc(user.uid).
This is the full code.
var htmlEmail = document.getElementById('email').value;
var htmlPass = document.getElementById('password').value;
var htmlUser = document.getElementById('username').value.toLowerCase();
var auth = firebase.auth();
var promise = auth.createUserWithEmailAndPassword(htmlEmail, htmlPass);
// If there is any error stop the process.
promise.catch(function (error) {
var errorCode = error.code;
console.log(`GOT ERROR: ` + errorCode)
if (errorCode == 'auth/weak-password') return // password to weak. Minimal 6 characters
if (errorCode == 'auth/email-already-in-use') return // Return a email already in use error
});
// When no errors create the account
promise.then(function () {
var userUid = auth.currentUser.uid;
var db = firebase.firestore();
db.collection('users').doc(userUid).set({
email: htmlEmail,
emailVertified: false,
name: htmlUser,
online: false,
onlock: false,
password: htmlPass
});
});
Then when the user logs you can simply request the data over the user.uid.
var auth = firebase.auth();
firebase.auth().onAuthStateChanged(function (user) {
// Lay connection with the database.
var firestore = firebase.firestore();
var db = firestore.collection('users').doc(user.uid);
// Get the user data from the database.
db.get().then(function (db) {
// Catch error if exists.
promise.catch(function (error) {
// Return error
});
promise.then(function () {
// continue when success
});
});
});
There could just be there are better ways. (still learning myself).
But this does the trick for me and works very well.
There are 2 things to keep in mind !
I would recommend Firestore over the real time database as it is faster and more secure.
Make sure your database rules are set correctly, so that no one can view / leak your database information. (as you are logging users personal info). If not set correctly users will be able to view your database and even purge all data.
Hope it helps :)
If you find a better way yourself please let us know in here.
We could learn from that also !
In a simplified way you can do this, everytime a user will signup this function will create a firestore collection with the specific parameters.
signupWithEmail: async (_, { email, password, name }) => {
var user = firebase.auth().createUserWithEmailAndPassword(email,
password).then(cred => {
return
firebase.firestore().collection('USERS').doc(cred.user.uid).set({
email,
name
})
})
return { user }
}

Generating a custom auth token with a cloud function for firebase using the new 1.0 SDK

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

How to listen for specific Firestore document creation event?

I am implementing a command/response pattern where the user writes to a command collection by calling add with a payload under his own userId, and then gets the data from a similar response path.
However the code below doesn't work, because onSnapshot can not listen for a document that hasn't yet been created (document command.id in the /responses/{userId}/register collection). This would be easy to solve with an onCreate handler, which exists for cloud functions but not for the JS firebase client API it seems.
This is using redux-firestore and some of my app helper functions, but you'll get the idea. The command and response document structures use { payload, error} similar to FSA
Cloud Function
export const register = functions.firestore
.document("commands/{userId}/register/{commandId}")
.onCreate(async event => {
const payload = event.data.get("payload");
const { userId, commandId } = event.params;
const response = db.document(`responses/${userId}/register/${commandId}`)
// possibly something bad will happen
try {
// do something with payload...
return response.set({
payload: "ok" // or pass relevant response data
})
} catch(err) {
return response.set({
error: true
payload: error
})
}
});
Client
export async function register(fs: any, userId: string) {
try {
// issue a new command
const command = await fs.add(
{ collection: `commands/${userId}/register` },
{ payload: fs.firestore.FieldValue.serverTimestamp() }
);
// wait for the response to be created
fs.onSnapshot(
{ collection: `responses/${userId}/register`, doc: command.id },
function onNext(doc) {
const {error, payload} = doc.data()
if (error) {
return notify.error({ title: 'Failed to register', message: payload.message });
}
notify.json(payload);
},
function onError(err) {
notify.error(err);
}
);
} catch (err) {
notify.error(err);
}
}
Is there no such thing as onCreate for web clients?
The only scalable solution I can think of is to store the response data as a child in the command document, but I think it is not as nice, because I suspect you can not make the permissions as strict then.
I would like the user only to be able to write to the command, and only read from the response paths. If I place the response as a child of command, this would not be possible I think?
I'm wondering if I'm not overlooking some API...

$firebase with userid reference, init after user login best practice?

Just like firefeed, i'm storing user-meta under /users/userid.
I only need the meta for the currently logged in user, so my thinking is to grab a reference only for the logged in user. So instead of
usersRef = new Firebase(firebase/users/) && users = $firebase(usersRef)
i'm waiting until the login service sets the current user, and then created the reference based on that user's id. This is inside of a service.
var userRef = undefined;
var user = undefined;
var _setCurrentUser = function (passedUser) {
console.log(passedUser);
currentUser = passedUser;
if (!currentUser) {
userRef = new Firebase(FIREBASE_URI + 'users/' + currentUser.id);
user = $firebase(userRef);
}
};
My question is: Is this a good idea? If i don't need a reference to the entire users object, does it make sense performance-wise to specify a specific user. How so/in what way? Is there a better way to wait until we have the current user's id to create the firebase instance for the user?
Ideally, if you don't need all users, you would fetch the specific reference. Something like the following:
var app = angular.module('myApp', ['firebase']);
// a little abstraction to manage establishing a $firebaseSimpleLogin instance
app.factory('loginService', function($firebaseSimpleLogin, Firebase) {
var fb = new Firebase(URL);
var auth = $firebaseSimpleLogin(fb);
return auth;
});
// a little abstraction to reduce the deps involved in creating a $firebase object
app.factory('syncData', function($firebase, Firebase) {
return function(pathToData) {
return $firebase(new Firebase(URL).child(pathToData));
}
});
app.factory('logInAndReturnUser', function(loginService, syncData) {
return function(provider) {
// call the login service
return loginService.$login(provider)
.then(function(user) {
// resolve to a $firebase object for the specific user
return syncData('users/'+user.uid);
});
}
});
Angular-ui's ui-router is ideal for this sort of use case and I highly recommend this approach for dealing with auth. Simply set up a resolve that returns the user:
var app = angular.module('myApp', ['firebase']);
app.factory('loginService', function($firebaseSimpleLogin, Firebase) {
var fb = new Firebase(URL);
var auth = $firebaseSimpleLogin(fb);
return auth;
});
app.configure(function($stateProvider) {
$stateProvider.state('home', {
url: '/',
resolve: {
'user': function(loginService) {
// getCurrentUser returns a promise that resolves to the user object
// or null if not logged in
return loginService.$getCurrentUser();
}
},
controller: 'homeCtrl'
})
});
app.controller('homeCtrl', function(user) {
// assumes we've logged in already, that can be part of router
// processing or we could check for user === null here and send to login page
console.log('user is ' + user.uid);
});

Resources