173
votes

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?

9
how did you get the custom token generated by Firebase Admin SDK in the first APIAmine Harbaoui
@AmineHarbaoui I had the same question. See this page: firebase.google.com/docs/auth/admin/verify-id-tokensMichM

9 Answers

171
votes

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.

149
votes

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.

28
votes

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

  // do your things..
  const uid = context.auth.uid;
  const query = data.query;

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

It can be invoked directly from you client like so:

firebase.functions().httpsCallable('getData')({query}).then(result => console.log(result));
7
votes

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.

0
votes

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
}
0
votes

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

0
votes

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.

0
votes

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;
    }
}
0
votes

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