How to protect firebase Cloud Function HTTP endpoint to allow only Firebase authenticated users?
H

9

223

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?

Hoppe answered 12/3, 2017 at 17:59 Comment(2)
how did you get the custom token generated by Firebase Admin SDK in the first APICheep
@AmineHarbaoui I had the same question. See this page: firebase.google.com/docs/auth/admin/verify-id-tokensAnticlinal
C
221

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.

Chondrule answered 12/3, 2017 at 20:14 Comment(9)
Is this code sample Still valid? Is this still how you would address this today?Agulhas
@GalBracha It should still be valid today (Oct 31 2017).Chondrule
@DougStevenson will those 'console.log' calls have any 'noticeable' impact on performance?Perforce
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.Ober
@Ober 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.Chondrule
I find that example terrible. Why would I want to create an express app within a function?Frigorific
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).Shows
In 2021, HTTPS Callable Firebase Functions should get the job done for most use cases.Lisettelisha
Callable functions unfortunately cannot set cache headers so can't use it to create db bundles for example.Dutchman
A
197

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.

Alsoran answered 12/6, 2017 at 13:23 Comment(10)
Upvoted as it is simple, and doesn't depend on express like the official example does.Metacenter
Can you explain more about the cors?Ciliata
@pete: cors is just solving cross-origin resource sharing. You can google to know more about it.Collegiate
@Ciliata Cors allows you to hit that firebase-backend end-point from different urls.Allysonalma
Is there a way to validate calls from our server using firebase admin sdk?Hallucinatory
How that token is generated?Pirate
@RezaRahmati You can use the getIdToken() method on client-side (e.g. firebase.auth().currentUser.getIdToken().then(token => console.log(token))) firebase docsAlsoran
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 ?Postconsonantal
You can find the most up-to-date information from this official document. firebase.google.com/docs/auth/admin/verify-id-tokensParticia
only works for nodejs, and callable functions, not any other function written in other languages - which you cant authenticate.Foolhardy
S
57

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));
Spier answered 1/7, 2019 at 14:40 Comment(4)
Does it check authentication for an app registered on the project, or would it allow any app using Firebase Auth ?Roomette
callable only works for nodejs functions.Foolhardy
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.Hydrobomb
Change condition to !context?.auth or you can get this error: Unhandled error TypeError: Cannot read properties of undefined (reading 'auth')Inelastic
S
14

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.

Shows answered 3/2, 2020 at 16:10 Comment(2)
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 :(Algie
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. @JanakaBandaraMend
S
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.

Solberg answered 25/6, 2020 at 8:53 Comment(0)
Q
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;
    }
}
Quart answered 14/12, 2020 at 14:33 Comment(0)
G
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
})
Goodson answered 23/6, 2021 at 16:58 Comment(0)
S
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
}
Seigel answered 28/2, 2020 at 22:28 Comment(0)
E
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

Erective answered 22/6, 2020 at 14:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.