How do I authenticate with my Google Cloud Function when using my Firebase App with Google Sign In?

Viewed 1252

I am a newbie (6 months going or so) and creating an app on Android (Java) that utilizes FireBase Auth with Google Sign In. (with only a few days of NodeJS exposure now) In other words my end user signs in with the Google Account. That part (I think) works pretty well so far. I use the Firestore Database heavily for a lot of things in the app.

So now I've gotten to the point where I want to use (Callable) Cloud Functions with HTTP Triggers. (never having done any of this before) I'm trying to get a proof of concept working at this time. The actual function works and I can trigger it from my app.

It appears that I cannot figure out how to make the function "private" though; as in "adding proper Members" to the Cloud function who have the right to invoke the function.

I have tried a few different things by trial error, but first let me show what I have.

This is the Cloud Function and I'm passing in an arbitrary String as a test, works nicely: (as long as "allUsers" have the role/right to invoke the function; in other words when the function is public.


exports.createTest = functions.https.onCall((data, context) => {

const text = data.text;

const uid = context.auth.uid;
const name = context.auth.token.name || null;
const email = context.auth.token.email || null;

console.log('UID: ', uid);
console.log('Name: ', name);
console.log('Email: ', email);
console.log('Message: ', text);



});

The above function gets triggered in my Android/Java code like this: (I think this code came from Google Doc/Sample/Example

    private FirebaseFunctions mFunctions;


    ...

    private void testing() {


        mFunctions = FirebaseFunctions.getInstance();

        Log.e(LOG_TAG, "Testing executed!");

        String testMessage = "Hello Hello Testing 123 Mic Check";


        createTest(testMessage)
                .addOnCompleteListener(new OnCompleteListener<String>() {
                    @Override
                    public void onComplete(@NonNull Task<String> task) {
                        if (!task.isSuccessful()) {
                            Exception e = task.getException();
                            if (e instanceof FirebaseFunctionsException) {
                                FirebaseFunctionsException ffe = (FirebaseFunctionsException) e;
                                FirebaseFunctionsException.Code code = ffe.getCode();
                                Object details = ffe.getDetails();

                                Log.e(LOG_TAG, "FFE: " + ffe.getMessage() );
                                Log.e(LOG_TAG, "Code: " + code);
                                Log.e(LOG_TAG, "Details:" + details);
                            }

                            // ...
                        }

                        // ...
                    }
                });

    }

    private Task<String> createTest(String text) {
        // Create the arguments to the callable function.
        Map<String, Object> data = new HashMap<>();
        data.put("text", text);
        data.put("push", true);

        return mFunctions
                .getHttpsCallable("createTest") //this is the function name
                .call(data)
                .continueWith(new Continuation<HttpsCallableResult, String>() {
                    @Override
                    public String then(@NonNull Task<HttpsCallableResult> task) throws Exception {
                        // This continuation runs on either success or failure, but if the task
                        // has failed then getResult() will throw an Exception which will be
                        // propagated down.
                        String result = (String) task.getResult().getData();

                        if (result != null) {
                            Log.e(LOG_TAG, "Result: " + result);
                        }
                        return result;
                    }
                });
    }

Only when I have "allUsers" added with the role/right to "invoke Cloud Function" then I get this working. My understanding of HTTP Requests and such is pretty limited, which is not making things easier.

I tried using the "allAuthenticatedUsers" options, which I figured would do the trick, because I actually authenticate my Users in the app through Firebase/Google Sign In. This Cloud Function shall only be available to either a) authenticated users or b) users of a specific domain. (I have a domain, let's say @testorganization.com) Or if I can identify my particular app (api key?) then that would work, too.

The moment I add a member "allAuthenticatedUsers" with role to invoke the function (and remove "allUsers) nothing happens. I also tried adding the entire domain, but that wouldn't work. (duh) Also tried adding my service account (trial and error at this point) and didn't seem to work.

In my Node JS code I am actually receiving the UID of the authenticated user, so it appears that some kind of user authentication information is already being exchanged.

With that knowledge, I can (successfully tried this) get the UID and cross check that against my database and verify a user that way, but seems unnecessary and I should be able to make the permissions work. (lock the function down entirely) Plus this took a really long time just finish this cross check. Or is this pretty standard procedure to do?

Like this-->

const usersRef =  admin.firestore().collection('users').doc(uid)

usersRef.get()
  .then((docSnapshot) => {
    if (docSnapshot.exists) {
      usersRef.onSnapshot((doc) => {
       console.log('User Type logged in: ', doc.data().userCategory)
       console.log('User Title: ', doc.data().userTitle)
      });
    } else {
      console.log('User does not exist')
    }

});

Edit:

So while not having figured out how to shut down the function entirely, I did discover that instead of cross checking my users, I can simple check for auth like this:

if (context.auth){
  //user is auth'd
} else {
  //no auth
}

That's a little bit better, I guess. (but still doesn't technically prevent access to the function?!)

Thank you so much for any help. Much appreciated.

Edit2:

Here is a screensshot of the area in the cloud console (for cloud function roles/privileges) that I am referring to:

https://imgur.com/cBsjaaL

1 Answers

With a Callable Cloud Function, if you want to ensure that only authenticated users can trigger it's business logic, you actually don't need to configure any extra "cloud function roles/privileges" as shown at the bottom of your question.

By default, with a Callable Cloud Function you will get, when available, "Firebase Authentication and FCM tokens automatically included in requests" and it will "automatically deserializes the request body and validates auth tokens", as explained in the doc.

So you just have to follow the doc and use the context parameter. As you have mentioned in your question, you can check the user is authenticated by doing:

if (context.auth) {
  //...
}

If you want to verify the user email, you would do:

exports.addMessage = functions.https.onCall((data, context) => {

    const uid = context.auth.uid;
    return admin.auth().getUser(uid)
      .then(userRecord => {
          const userEmail = userRecord.email;
          //....
       })
       .catch(function(error) {
           console.log('Error fetching user data:', error);
          // Send back an error to the front end
          // See https://firebase.google.com/docs/functions/callable#handle_errors
       });

});

You will find more examples on how to "work with" users with the Admin SDK here in the doc.

Related