Authenticating Cloud Functions


I think that there is no need to explain what a cloud function is. If you need to know more about cloud functions, there are some articles in this bloc and documentation provided by google will also really help. For proceed with this article you might need to have a little knowledge about writing and deploying cloud functions. If you are new to cloud functions, take a look at the cloud function documentation from Firebase. I find it really easy compared to the GCP cloud function documentation.


Introduction

When talking about securing a cloud function, there are 3 types of protection that you need to worry about.

  1. Giving access to a specific set of users, possibly developers.
  2. Giving access to another function as cloud functions can be triggered with other functions without any human interaction.
  3. Giving access to the end users of the product.

In this blog, I would like to talk about giving access to the end users of the system. I will discuss the other ways of securing the cloud function in another blog.

The system needs to have Firebase Authentication for securing the access. Users can use the system only if they are authenticated. Here I will discuss how to give cloud function access (authorize) only for the users who have authenticated in the system. I will focus on cloud functions that can be triggered with HTTPS request. But you can do some modifications and use it for callable cloud functions also.


Basic Backend Function

First you need to set up your HTTPS cloud function. Then you need to open the required endpoint. I will use typescript for the examples. The following example will open an endpoint and upon a POST request, it will return "Hello World" as the response.

1
2
3
4
5
6
7
8
9
10
11
12
13
const functions = require('firebase-functions');
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
import {Request, Response} from 'express';

const app = express();
app.use(cors({ origin: true }));
app.use(cookieParser());

app.post('/', (req: Request, res: Response) => res.send("Hello World\n"));

exports.api = functions.https.onRequest(app);

You can deploy the cloud function with npm run deploy. Then you can run the following cURL command to verify whether it is working. Make sure to change the request URL to your cloud function.

1
curl --location --request POST 'https://us-central1-fcode-blog.cloudfunctions.net/api'

You will see Hello World text in the console if everything is correct.


Authorization Process

Without going into the next step, I will explain how authorization is done.

  1. When the Rest API is called from the client side (Web or Mobile), client should send a special token called a ID token which can be obtained only for authenticated users from the Firebase Authentication client SDK or API.
  2. This is an encrypted key which can be used to uniquely identify users.
  3. Server will analyze the key and identify which user is making this request. If a user to this token cannot be found, send an unauthorized response to the client.

This way of authorizing a user is called bearer token authorization. Therefore we send this ID token in the header as Authorization: Bearer ID_TOKEN.


Applying a middleware

We can create an express middleware and block any request that doesn’t have a valid authorization header.

1
2
3
const validateToken = async (req: Request, res: Response, next: Function) => {
console.log('Check if request is authorized with Firebase ID token');
}

We can use the above middleware to validate the ID token. Then add that middleware to the express object.

1
2
3
4
const app = express();
app.use(cors({ origin: true }));
app.use(cookieParser());
app.use(validateToken);

Validating the ID token

First we need to check if the ID token is there in the header or in the cookies. If not we can return an unauthorized error back to the client.

1
2
3
4
5
6
7
8
9
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;
}

Then we can extract the id token from the header or from the cookie.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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;
}

Finally, we need to verify the extracted token is a valid one. For that we use Firebase Authentication Admin SDK.

1
const decodedIdToken = await admin.auth().verifyIdToken(idToken);

All the properties of the returning object (named as decodedIdToken) can be found in this documentation. Main properties that you will need are email, phone_number, auth_time and uid. From these details, you can uniquely identify the user who made this API call. For that reason, you may need to store this object in the request so that end point implementation can access it.

After all these steps, our middleware will look like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const validateToken = async (req: Request, res: Response, next: Function) => {
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;
}
}

Then we can read the properties of the decodedIdToken within our API. Without just sending "Hello World" as the success response, we can do the following. Also as we are using firebase admin SDK, we need to import and initialize that as well.

1
2
3
4
const admin = require('firebase-admin');
if (admin.apps.length === 0) admin.initializeApp();

app.post('/', (req: Request, res: Response) => res.send(`Hello, ${req.user?.email}.`));

Fixing Lint Errors

Since we are using typescript, you can’t just add properties to a already defined objects. In that case req.user = decodedIdToken; line will give you an error. Therefore we need a trick to change the definition of the express Request class. Create a file named index.d.ts in functions/@types/express folder. You might need to create the folder. Then add the following.

1
2
3
4
5
6
7
8
9
10
import {Request} from 'express';
import * as admin from "firebase-admin";
import DecodedIdToken = admin.auth.DecodedIdToken;

declare module 'express' {

interface Request {
user?: DecodedIdToken
}
}

Then we have to tell TSLint that we are overriding some of the object definitions within our code. To do that, open functions/tsconfig.json and add the following lines under compilerOptions. (Do not remove the existing key-values. Just add these.)

1
2
3
4
5
"allowSyntheticDefaultImports": true,
"typeRoots": [
"@types",
"./node_modules/@types"
]

Testing the middleware

After you deploy the new version, try the first cURL command to check whether this API still accepting unauthorized requests. You will see that it is not.

Then you need to create a test user in the Firebase Authentication server. I will create a user with email ramesh@example.com and password 123456789. Then use the firebase REST API to login with email and password. To use firebase REST API, you will need a API key. You can get that in the Project Settings page under Web API Key field in the Firebase Console. Copy that value and replace API_KEY in the following cURL command.

1
2
3
4
5
6
7
curl --location --request POST 'https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key=API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"email": "ramesh@example.com",
"password": "123456789",
"returnSecureToken": true
}'

When you run the above command you will get a json object as the response. The value we are interested in is the idToken of the response. Copy that and replace ID_TOKEN of the following cURL command with it.

1
2
curl --location --request POST 'https://us-central1-fcode-blog.cloudfunctions.net/api' \
--header 'Authorization: Bearer ID_TOKEN'

After you execute the cURL command, you will see the intended output as Hello, ramesh@example.com..


Client Side

When we are accessing this end point using the client side, we need to get the ID_TOKEN for authenticated users. You saw how to get that if you are using the Authentication REST API from the above testing section. This section will describe how to get it from the Firebase Authentication SDK.

In flutter SDK you can use FirebaseAuth.currentUser to get the authenticated user. Then call the getIdToken() method of the returned user.

In web SDK you can use Auth.currentUser to get the authenticated user. Then call the getIdToken() method of the returned user.

In android SDK you can use FirebaseAuth.getCurrentUser() to get the authenticated user. Then call the getIdToken() method of the returned user to get a Task. After that Task is completed, call the getToken() method.

In iOS SDK you can use Auth.currentUser to get the authenticated user. Then call getIdTokenResult() method of the returned user.

Use the obtained ID token in the Authorization header.


Conclusion

I think I covered lot of things in this blog. I hope I explained every step very well so that you know why you did it, instead of just copying and paste internet code. Most important thing is to understand why I did those things in each steps. Then you can modify and apply that knowledge into anything.

Anyway, if you have any more questions, feel free to add a comment. I pushed my code into a github repo. You can goto that with the following link.

Authenticating Cloud Functions

Authenticating Cloud Functions

This repository contains a sample code to secure your cloud functions using firebase authentication.

github.com