217

With the new firebase cloud function I've decided to move some of my HTTP endpoint to firebase. Everything works great... But i have the following issue. I have two endpoints build by HTTP Triggers (Cloud Functions)

  1. An API endpoint to create users and returns the custom Token generated by Firebase Admin SDK.
  2. An API endpoint to fetch certain user details.

While the first endpoint is fine, but for my second end point i would want to protect it for authenticated users only. meaning someone who has the token i generated earlier.

How do i go about solving this?

I know we can get the Header parameters in the cloud function using

request.get('x-myheader')

but is there a way to protect the endpoint just like protecting the real time data base?

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
spaceMonkey
  • 4,475
  • 4
  • 26
  • 34

9 Answers9

214

There is an official code sample for what you're trying to do. What it illustrates is how to set up your HTTPS function to require an Authorization header with the token that the client received during authentication. The function uses the firebase-admin library to verify the token.

Also, you can use "callable functions" to make a lot of this boilerplate easier, if your app is able to use Firebase client libraries.

thdoan
  • 18,421
  • 1
  • 62
  • 57
Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • 4
    Is this code sample Still valid? Is this still how you would address this today? – Gal Bracha Oct 31 '17 at 00:04
  • 1
    @GalBracha It should still be valid today (Oct 31 2017). – Doug Stevenson Oct 31 '17 at 06:05
  • @DougStevenson will those 'console.log' calls have any 'noticeable' impact on performance? – Sanka Darshana Dec 01 '17 at 04:06
  • 3
    How will using callable functions make the boilerplate easier? From what I understand those are just "non-REST" server functions, I don't really understand how they relate here. Thanks. – 1252748 Feb 18 '20 at 19:13
  • 3
    @1252748 If you read the linked documentation, it will become clear. It handles the passing and validation of the auth token automatically, so you don't have to write that code on either side. – Doug Stevenson Feb 18 '20 at 19:17
  • 6
    I find that example terrible. Why would I want to create an express app within a function? – s.alem Apr 13 '20 at 11:58
  • Note: you can actually restrict access from *outside* the function, i.e. before function invocation, *if* your users are authenticated via a Google account. (See my answer way down for more detail). – ultraGentle Mar 02 '21 at 04:05
  • In 2021, [HTTPS Callable Firebase Functions](https://firebase.google.com/docs/functions/callable) should get the job done for most use cases. – Android Dec 31 '21 at 23:40
  • Callable functions unfortunately cannot set cache headers so can't use it to create db bundles for example. – eozzy Mar 18 '22 at 19:30
189

As mentioned by @Doug, you can use firebase-admin to verify a token. I've set up a quick example:

exports.auth = functions.https.onRequest((req, res) => {
  cors(req, res, () => {
    const tokenId = req.get('Authorization').split('Bearer ')[1];
    
    return admin.auth().verifyIdToken(tokenId)
      .then((decoded) => res.status(200).send(decoded))
      .catch((err) => res.status(401).send(err));
  });
});

In the example above, I've also enabled CORS, but that's optional. First, you get the Authorization header and find out the token.

Then, you can use firebase-admin to verify that token. You'll get the decoded information for that user in the response. Otherwise, if the token isn't valid, it'll throw an error.

ManSamVampire
  • 153
  • 2
  • 6
Will
  • 2,523
  • 1
  • 13
  • 21
  • 28
    Upvoted as it is simple, and doesn't depend on express like the official example does. – DarkNeuron Jun 28 '17 at 15:00
  • 5
    Can you explain more about the cors? – pete Jul 10 '17 at 06:18
  • @pete: cors is just solving cross-origin resource sharing. You can google to know more about it. – Lạng Hoàng Sep 28 '17 at 02:15
  • @pete Cors allows you to hit that firebase-backend end-point from different urls. – Walter Monecke Dec 15 '17 at 12:43
  • Is there a way to validate calls from our server using firebase admin sdk? – aloj Apr 10 '18 at 07:51
  • How that token is generated? – Reza Oct 13 '18 at 18:55
  • 11
    @RezaRahmati You can use the `getIdToken()` method on client-side (e.g. `firebase.auth().currentUser.getIdToken().then(token => console.log(token))`) [firebase docs](https://firebase.google.com/docs/reference/js/firebase.User#getIdToken) – Will Oct 13 '18 at 21:10
  • When I try this code, I get a timeout in the logs and this as output : "Error: could not handle the request" I've added some logging, I can see the log before the cors(...), but not the logs right before "const tokenId = " Any idea ? – Thomas Feb 26 '19 at 15:36
  • You can find the most up-to-date information from this official document. https://firebase.google.com/docs/auth/admin/verify-id-tokens – 김준호 Dec 31 '21 at 06:57
  • only works for nodejs, and callable functions, not any other function written in other languages - which you cant authenticate. – gotiredofcoding Feb 10 '23 at 09:28
55

As also mentioned by @Doug, you can use Callable Functions in order to exclude some boilerplate code from your client and your server.

Example callable function:

export const getData = functions.https.onCall((data, context) => {
  // verify Firebase Auth ID token
  if (!context?.auth) {
    return { message: 'Authentication Required!', code: 401 };
  }

  /** This scope is reachable for authenticated users only */

  return { message: 'Some Data', code: 200 };
});

It can be invoked directly from you client like so:

firebase.functions().httpsCallable('getData')({query}).then(result => console.log(result));
Benny
  • 749
  • 7
  • 12
  • Does it check authentication for an app registered on the project, or would it allow any app using Firebase Auth ? – SkR Aug 26 '22 at 12:04
  • callable only works for nodejs functions. – gotiredofcoding Feb 10 '23 at 09:27
  • Just remember that you cannot cache responses for callable functions, as they are all done via HTTP POST. So if you are looking to avoid hitting the cloud function after an initial request, you may need to reconsider. Ultimately, if this is not an issue, the tradeoff is definitely worth it, IMO, with all the built-in functionality that callable functions offer. – zanona Mar 17 '23 at 08:55
  • Change condition to `!context?.auth` or you can get this error: `Unhandled error TypeError: Cannot read properties of undefined (reading 'auth')` – thdoan Jun 09 '23 at 23:03
13

The above methods authenticate the user using logic inside the function, so the function must be still be invoked to do the checking.

That's a totally fine method, but for the sake of comprehensivity, there is an alternative:

You can set a function to be "private" so that it can't be invoked except by registered users (you decide on permissions). In this case, unauthenticated requests are denied outside the context of the function, and the function is not invoked at all.

Here are references to (a) Configuring functions as public/private, and then (b) authenticating end-users to your functions.

Note that the docs above are for Google Cloud Platform, and indeed, this works because every Firebase project is also a GCP project. A related caveat with this method is that, as of writing, it only works with Google-account based authentication.

ultraGentle
  • 5,084
  • 1
  • 19
  • 45
  • 1
    Unfortunate that Firebase still doesn't seem to support this OOTB for Firebase users - if one passes an auth token, fine; but if not, the function gets invoked nonetheless :( – Janaka Bandara Mar 02 '21 at 01:15
  • So basically if we remove the `Allow unauthenticated` by removing the `allUsers` role, we cant use a callable function (`onCall`)? I don't think leaving the functions public is ideal, there must be a way to set the functions to be callable from specific service accounts. @JanakaBandara – MorenoMdz Oct 15 '21 at 17:37
3

In Firebase, in order to simplify your code and your work, it's just a matter of architectural design:

  1. For public accessible sites/contents, use HTTPS triggers with Express. To restrict only samesite or specific site only, use CORS to control this aspect of security. This make sense because Express is useful for SEO due to its server-side rendering content.
  2. For apps that require user authentication, use HTTPS Callable Firebase Functions, then use the context parameter to save all the hassles. This also makes sense, because such as a Single Page App built with AngularJS -- AngularJS is bad for SEO, but since it's a password protected app, you don't need much of the SEO either. As for templating, AngularJS has built-in templating, so no need for sever-side template with Express. Then Firebase Callable Functions should be good enough.

With the above in mind, no more hassle and make life easier.

Antonio Ooi
  • 1,601
  • 1
  • 18
  • 32
2

You can take this as a functions returns boolean. If the user verified or not then you will continue or stop your API. In Addition you can return claims or user result from the variable decode

const authenticateIdToken = async (
    req: functions.https.Request,
    res: functions.Response<any>
) => {
    try {
        const authorization = req.get('Authorization');
        if (!authorization) {
            res.status(400).send('Not Authorized User');
            return false;
        }
        const tokenId = authorization.split('Bearer ')[1];

        return await auth().verifyIdToken(tokenId)
            .then((decoded) => {
                return true;
            })
            .catch((err) => {
                res.status(401).send('Not Authorized User')
                return false;
            });
    } catch (e) {
        res.status(400).send('Not Authorized User')
        return false;
    }
}
Gander
  • 1,854
  • 1
  • 23
  • 30
Ali Shoman
  • 23
  • 2
2

There is a lot of great information here that really helped me, but I thought it might be good to break down a simple working example for anyone using Angular attempting this for the first time. The Google Firebase documentation can be found at https://firebase.google.com/docs/auth/admin/verify-id-tokens#web.

//#### YOUR TS COMPONENT FILE #####
import { Component, OnInit} from '@angular/core';
import * as firebase from 'firebase/app';
import { YourService } from '../services/yourservice.service';

@Component({
  selector: 'app-example',
  templateUrl: './app-example.html',
  styleUrls: ['./app-example.scss']
})

export class AuthTokenExample implements OnInit {

//property
idToken: string;

//Add your service
constructor(private service: YourService) {}

ngOnInit() {

    //get the user token from firebase auth
    firebase.auth().currentUser.getIdToken(true).then((idTokenData) => {
        //assign the token to the property
        this.idToken = idTokenData;
        //call your http service upon ASYNC return of the token
        this.service.myHttpPost(data, this.idToken).subscribe(returningdata => {
            console.log(returningdata)
        });

    }).catch((error) => {
        // Handle error
        console.log(error);
    });

  }

}

//#### YOUR SERVICE #####
//import of http service
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';

@Injectable({
  providedIn: 'root'
})

export class MyServiceClass {

    constructor(private http: HttpClient) { }

  //your myHttpPost method your calling from your ts file
  myHttpPost(data: object, token: string): Observable<any> {

    //defining your header - token is added to Authorization Bearer key with space between Bearer, so it can be split in your Google Cloud Function
    let httpOptions = {
        headers: new HttpHeaders({
            'Content-Type': 'application/json',
         'Authorization': 'Bearer ' + token
        })
    }

    //define your Google Cloud Function end point your get from creating your GCF
    const endPoint = ' https://us-central1-your-app.cloudfunctions.net/doSomethingCool';

    return this.http.post<string>(endPoint, data, httpOptions);

  }

}


//#### YOUR GOOGLE CLOUD FUNCTION 'GCF' #####
//your imports
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const cors = require('cors')({origin: true});


exports.doSomethingCool = functions.https.onRequest((req, res) => {

//cross origin middleware
    cors(req, res, () => {

        //get the token from the service header by splitting the Bearer in the Authorization header 
        const tokenId = req.get('Authorization').split('Bearer ')[1];

        //verify the authenticity of token of the user
        admin.auth().verifyIdToken(tokenId)
            .then((decodedToken) => {
                //get the user uid if you need it.
               const uid = decodedToken.uid;

                //do your cool stuff that requires authentication of the user here.

            //end of authorization
            })
            .catch((error) => {
                console.log(error);
            });

    //end of cors
    })

//end of function
})
Steve Klock
  • 115
  • 1
  • 8
1

There is a nice official example on it using Express - may be handy in future: https://github.com/firebase/functions-samples/blob/master/authorized-https-endpoint/functions/index.js (pasted below just for sure)

Keep in mind that exports.app makes your functions available under /app slug (in this case there is only one function and is available under <you-firebase-app>/app/hello. To get rid of it you actually need to rewrite Express part a bit (middleware part for validation stays the same - it works very good and is quite understandable thanks to comments).

/**
 * Copyright 2016 Google Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict';

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();
const express = require('express');
const cookieParser = require('cookie-parser')();
const cors = require('cors')({origin: true});
const app = express();

// Express middleware that validates Firebase ID Tokens passed in the Authorization HTTP header.
// The Firebase ID token needs to be passed as a Bearer token in the Authorization HTTP header like this:
// `Authorization: Bearer <Firebase ID Token>`.
// when decoded successfully, the ID Token content will be added as `req.user`.
const validateFirebaseIdToken = async (req, res, next) => {
  console.log('Check if request is authorized with Firebase ID token');

  if ((!req.headers.authorization || !req.headers.authorization.startsWith('Bearer ')) &&
      !(req.cookies && req.cookies.__session)) {
    console.error('No Firebase ID token was passed as a Bearer token in the Authorization header.',
        'Make sure you authorize your request by providing the following HTTP header:',
        'Authorization: Bearer <Firebase ID Token>',
        'or by passing a "__session" cookie.');
    res.status(403).send('Unauthorized');
    return;
  }

  let idToken;
  if (req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
    console.log('Found "Authorization" header');
    // Read the ID Token from the Authorization header.
    idToken = req.headers.authorization.split('Bearer ')[1];
  } else if(req.cookies) {
    console.log('Found "__session" cookie');
    // Read the ID Token from cookie.
    idToken = req.cookies.__session;
  } else {
    // No cookie
    res.status(403).send('Unauthorized');
    return;
  }

  try {
    const decodedIdToken = await admin.auth().verifyIdToken(idToken);
    console.log('ID Token correctly decoded', decodedIdToken);
    req.user = decodedIdToken;
    next();
    return;
  } catch (error) {
    console.error('Error while verifying Firebase ID token:', error);
    res.status(403).send('Unauthorized');
    return;
  }
};

app.use(cors);
app.use(cookieParser);
app.use(validateFirebaseIdToken);
app.get('/hello', (req, res) => {
  res.send(`Hello ${req.user.name}`);
});

// This HTTPS endpoint can only be accessed by your Firebase Users.
// Requests need to be authorized by providing an `Authorization` HTTP header
// with value `Bearer <Firebase ID Token>`.
exports.app = functions.https.onRequest(app);

My rewrite to get rid of /app:

const hello = functions.https.onRequest((request, response) => {
  res.send(`Hello ${req.user.name}`);
})

module.exports = {
  hello
}
jean d'arme
  • 4,033
  • 6
  • 35
  • 70
1

I have been struggling to get proper firebase authentication in golang GCP function. There is actually no example for that, so I decided to build this tiny library: https://github.com/Jblew/go-firebase-auth-in-gcp-functions

Now you can easily authenticate users using firebase-auth (which is distinct from gcp-authenticated-functions and is not directly supported by the identity-aware-proxy).

Here is an example of using the utility:

import (
  firebaseGcpAuth "github.com/Jblew/go-firebase-auth-in-gcp-functions"
  auth "firebase.google.com/go/auth"
)

func SomeGCPHttpCloudFunction(w http.ResponseWriter, req *http.Request) error {
   // You need to provide 1. Context, 2. request, 3. firebase auth client
  var client *auth.Client
    firebaseUser, err := firebaseGcpAuth.AuthenticateFirebaseUser(context.Background(), req, authClient)
    if err != nil {
    return err // Error if not authenticated or bearer token invalid
  }

  // Returned value: *auth.UserRecord
}

Just keep in mind to deploy you function with --allow-unauthenticated flag (because firebase authentication occurs inside function execution).

Hope this will help you as it helped me. I was determined to use golang for cloud functions for performance reasons — Jędrzej

jblew
  • 506
  • 3
  • 9