27

In the example bellow, is there a way to get the user id (uid) of the user who wrote to 'offers/{offerId}'? I tried to do as described here but it doesn't work in Firestore.

exports.onNewOffer = functions.firestore
  .document('offers/{offerId}')
  .onCreate(event => {
    ...
});
Nicholas
  • 501
  • 2
  • 14
Gabriel Bessa
  • 468
  • 1
  • 4
  • 8

10 Answers10

34

I was struggling on this for a while and finally contacted the firebase Support:

I was trying to achieve this.

The event.auth.uid is undefined in the event object for firestore database triggers. (It works for the realtime Database Triggers)

When I console.log(event) I can’t find any auth in the output.

The official support answer:

Sorry the auth is not yet added in the Firestore SDK. We have it listed in the next features.

Keep an eye out on our release notes for any further updates.

I hope this saves someone a few hours.

UPDATE:

The issue has been closed and the feature will never be implemeted:

Hi there again everyone - another update. It has been decided that unfortunately native support for context.auth for Firestore triggers will not be implemented due to technical constraints. However, there is a different solution in the works that hopefully will satisfy your use case, but I cannot share details. On this forum we generally keep open only issues that can be solved inside the functions SDK itself - I've kept this one open since it seemed important and I wanted to provide some updates on the internal bugs tracking this work. Now that a decision has been reached, I'm going to close this out. Thanks again for everyone's patience and I'm sorry I don't have better news. Please use the workaround referenced in here.

Community
  • 1
  • 1
Tim
  • 438
  • 4
  • 10
  • 2
    There's an open issue for this: https://github.com/firebase/firebase-functions/issues/300#issuecomment-428421203 – Christian Oct 10 '18 at 14:24
  • hi, my cloud function end-point looks different and has export const getUsersInThisChatRoom = functions.https.onRequest(async function(req,res) there is no req.auth.uid and no event. let uid = admin.auth().getUser(req.auth.uid); anything I'm missing? – aman Apr 03 '19 at 17:59
  • i see you got a if statement to avoid loop. But what if the client adds the createdAt field manually? I think this check shouldnt be there. correct me if i am wrong. but i belive you want to add the timestamp on the server and avoid the client from adding it – Mustafa Nov 15 '19 at 11:31
  • 2
    The auth object will never get implemented for Firestore :( https://github.com/firebase/firebase-functions/issues/300#issuecomment-465471983 – DarkNeuron Jan 16 '20 at 16:25
8

Summary of how I solved this / a workable solution:

On client

Add logged in/current user's uid (e.g. as creatorId) to entity they're creating. Access this uid by storing the firebase.auth().onAuthStateChanged() User object in your app state.

In Firebase Firestore/Database

Add a Security Rule to create to validate that the client-supplied creatorId value is the same as the authenticated user's uid; Now you know the client isn't spoofing the creatorId and can trust this value elsewhere.

e.g.

match /entity/{entityId} {
  allow create: if madeBySelf();
}

function madeBySelf() {
  return request.auth.uid == request.resource.data.creatorId;
}

In Firebase Functions

Add an onCreate trigger to your created entity type to use the client-supplied, and now validated, creatorId to look up the creating user's profile info, and associate/append this info to the new entity doc.

This can be accomplished by:

  1. Creating a users collection and individual user documents when new accounts are created, and populating the new user doc with app-useful fields (e.g. displayName). This is required because the fields exposed by the Firebase Authentication system are insufficient for consumer app uses (e.g., displayName and avatarURL are not exposed) so you can't just rely on looking up the creating user's info that way.

    e.g. (using ES6)

import * as functions from 'firebase-functions'
import * as admin from 'firebase-admin'

const APP = admin.initializeApp()

export const createUserRecord = functions.auth.user()
  .onCreate(async (userRecord, context) => {
    const userDoc = {
      id: userRecord.uid,
      displayName: userRecord.displayName || "No Name",
      avatarURL: userRecord.photoURL || '',
    }
    return APP.firestore().collection('users').doc(userRecord.uid).set(userDoc)
  })
  1. Now that you have a validated creatorId value, and useful user objects, add an onCreate trigger to your entity type (or all your created entities) to look up the creating user's info and append it to the created object.
export const addCreatorToDatabaseEntry = functions.firestore
  .document('<your entity type here>/{entityId}')
  .onCreate(async (snapshot, context) => {
    const userDoc = await APP.firestore().collection('users').doc(snapshot.data().creatorId).get()
    return snapshot.ref.set({ creator: userDoc.data() }, { merge: true })
})

This clearly leads to a lot of duplicated user info data throughout your system -- and there's a bit of clean up you can do ('creatorId` is duplicated on the created entity in the above implementation) -- but now it's super easy to show who created what throughout your app, and appears to be 'the Firebase way'.

Hope this helps. I've found Firebase to be super amazing in some ways, and make some normally easy things (like this) harder than they 'should' be; on balance though am a major fan.

Ethan
  • 9,558
  • 5
  • 27
  • 24
6

The documentation states clearly that the context.auth param is only available in the Realtime Database.

This field is only populated for Realtime Database triggers and Callable functions. For an unauthenticated user, this field is null. For Firebase admin users and event types that do not provide user information, this field does not exist.

Personally I realized that I had the userId already in the path of my data.

export const onCreate = functions.firestore.document('docs/{userId}/docs/{docId}')
    .onCreate((snapshot, context) => {
        const userId = context.params.userId;
philipp
  • 4,133
  • 1
  • 36
  • 35
5

Until this is added to firestore functions a workaround is to add the user_id as a field when creating a document then deleting after. You can then grab it in the function onCreate then after you use it for what you need it for, while still in the function, just delete the field from that document.

askilondz
  • 3,264
  • 2
  • 28
  • 40
  • Sure, and if you want to track updates, you can have an `updatedBy` field that is always set by the client when updating the document. But this doesn't help if you want to track document deletes. You'd have to come up with more ugly workarounds, where maybe you have a separate collection that you move deleted documents into, or never actually delete documents but instead have a `deleted` property that gets set to true. – aldel Jun 30 '21 at 21:43
4

As already suggested above, the workaround will be to add a user_id field with the data itself and then read it on the server.

The drawback with this approach will be a security loophole. As we are not verifying the user id on the server, any other user will be able to impersonate other users by sending their id with the data.

For security critical applications, the solution for this will be to use security rules to verify that the correct user_id has been sent with the data

allow write: if request.resource.data.user_id == request.auth.uid;
Akshay Jain
  • 884
  • 5
  • 19
  • 1
    the proper rule should check request.resource and not resource (value before change) `allow write: if request.resource.data.user_id == request.auth.uid;` – Guillaume Gros Nov 26 '20 at 16:00
1

You could add your data via Callable function, from which you can access current user id:

exports.addNewOffer = functions.https.onCall(async (data, context) => {
  const uid = context.auth.uid
  const writeResult = await admin.firestore().collection('offers').add({ ... })
  ...
  return { id: writeResult.id, ... }
})
0

What is about snap._fieldsProto.uid.stringValue

Example:

exports.hello = functions.firestore.document('hello/{worldId}').onCreate((snap, context) => {
   console.log(snap._fieldsProto.uid.stringValue)
});
l2aelba
  • 21,591
  • 22
  • 102
  • 138
  • I get the same error. This isn't working! `TypeError: Cannot read properties of undefined (reading 'uid')` – Paul Jan 02 '22 at 17:39
0

This should do the trick:

exports.newMessage = functions.firestore
        .document('groups/messages/{messageId}')
        .onCreate((snap, context) => {
            var fromId = snap.data().fromId;
Doron Goldberg
  • 643
  • 1
  • 9
  • 23
  • 1
    your function should always points towards the document level,not towards the collection level – Dev Nov 30 '19 at 08:53
0

to access auth info for the user that requested the write put whatever info you need inside the document then delete it on the onWrite trigger then return change.after.ref.set(modifiedData);

if the field contains data then it means that the request was made by your code if not it could be an admin-sdk request or an firebase console request

dsl400
  • 322
  • 3
  • 14
-3

You can get the current signed-in user tokenId by calling getIdToken() on the User: https://firebase.google.com/docs/reference/functions/functions.auth.UserInfo

J Prakash
  • 1,323
  • 8
  • 13