Skip to content

Latest commit

 

History

History
281 lines (193 loc) · 9.77 KB

File metadata and controls

281 lines (193 loc) · 9.77 KB

Complete Firebase Cloud Functions TypeScript Example

A Node.js REST API example for Firebase, built with TypeScript, Express, Firebase Authentication, Firebase Admin SDK, and Firestore. It also handles Event Triggers (2nd gen) so all your code is organized. This project fits well to be used as a template for the creation of new servers.

The main aspects of this sample are:

  • An API HTTP Trigger:

    • A well-organized API under the api folder
    • Access Control: Reject user access by simply choosing what user roles can access a specific path or easily check the claims with a custom request object in the Request Handler
    • Reject a request anywhere by throwing new HttpResponseError(status, codeString, message)
  • Events Triggers (2nd gen):

    • A well-organized Events Triggers under the event-triggers folder
  • Shared components between API and Event Triggers are under the core folder

About this example

This example is a good start if you are building a Firebase Cloud Functions project.

About the 2nd gen event triggers example

Every time a user or product is created, or a product is updated, a new record is created in the db-changes Firestore Collection that only admins can access, the code for these triggers is inside the event-triggers folder.

The triggers are onUserCreated, onProductCreated, and onProductUpdated.

About the api HTTP trigger

There are three roles: storeOwner, buyer and admin. Anyone can create an account, but an adminKey is required to create a user with admin role.

What each user can do

Store Owners:

  • ✅ Create products
  • ✅ List public products data
  • ✅ Get full data of his own product
  • ❌ Get full data of other store owners' product
  • ❌ List records of changes made inside the DB, like "Product Blouse has been updated"

Buyers:

  • ✅ List public products data
  • ❌ Create products
  • ❌ Get full data of a product
  • ❌ List records of changes made inside the DB, like "Product Blouse has been updated"

Admins:

  • ✅ Create products
  • ✅ List public products data
  • ✅ Get full data of ANY product
  • ✅ List records of changes made inside the DB, like "Product Blouse has been updated"

Getting Started

In the Firebase Console:

  1. Go to Build > Authentication > Get Started > Sign-in method > Email/Password and enable Email/Password and save it.

  2. Also go to Build > Firestore Database > Create database. You can choose the option Start in test mode

Deploying

Go to the functions folder and run npm install to install the dependencies. After that, go back to the root folder (cd ..) and run:

  • npm install -g firebase-tools to install the Firebase CLI
  • firebase use --add and select your Firebase project, add any alias you prefer
  • And finally, run firebase deploy

API Authentication

Firebase Authentication is used to verify if the client is authenticated on Firebase Authentication, to do so, the client side should inform the Authorization header:

Authorization Header

The client's ID Token on Firebase Authentication in the format Bearer <idToken>, it can be obtained on the client side after the authentication is performed with the Firebase Authentication library for the client side. It can be generated by the client side only.

Option 1: Generating ID Token with Postman:

Follow the previous instructions on Use Postman to test it and pass it as Authorization header value in the format Bearer <idToken>

Option 2: Generating ID Token with a Flutter Client:

final idToken = await FirebaseAuth.instance.currentUser!.getIdToken();
// use idToken as `Authorization` header value in the format "Bearer <idToken>"

Option 3: Generating ID Token with a Web Client:

const idToken = await getAuth(firebaseApp).currentUser.getIdToken();
// use idToken as `Authorization` header value in the format "Bearer <idToken>"

Testing

Option 1: Testing with Remote DB

To make tests remotely, check what is your remote functions URL: in the Firebase Console go to Functions > and check the api url, it ends with .cloudfunctions.net/api.

Option 2: Testing Locally with a local emulator

In case you want to make tests locally using the Firebase Emulator, you can run npm run emulator inside the functions folder.

⚠️ Changes in the local emulator won't affect the remote db.

Open the Emulator UI on http://127.0.0.1:3005 > Functions emulator > and on the first lines check the http function initialized... log, it shows your Local URL, it ends with /api.

Using Postman to test it

1. Import the postman_collection.json file to your Postman

2. Right-click on the Postman collection you previously imported, click on Edit > Variables and on api replace the Current Value with your API URL.

Make sure the URL ends with /api and remember that if you use the local emulator URL it won't affect the remote db.

If you are testing using the local emulator, it will look something like: http://127.0.0.1:<port>/<your-project-id>/<region>/api

But if you are testing using the remote db, it will look like: https://<your-project-id>.cloudfunctions.net/api

3. Create an account on the 1. Create Account Postman Request

4. Follow the login steps to get an ID Token on Postman:

It's better to use a library of Firebase Authentication on the Client Side to get the ID Token, but let's use this method for testing because we are using Postman only

  • 4.1. In the Firebase Console > Go to Project Overview and Add a new Web platform

  • 4.2. Add a Nickname like "Postman" and click on Register App

  • 4.3. Copy only the apiKey field inside the firebaseConfig object

  • 4.4 Let's get the Firebase Authentication Token, on Postman, go to 2. Login on Google APIS request example and pass the apiKey as Query Param, edit the body with your email and password and click on Send, you will obtain an idToken as the response.

  • 4.5 For the other requests, the idToken should be set in the Authorization header (type Bearer). Let's set it as Postman variable too, so right-click on the Postman collection Edit > Variables and on idToken replace the Current Value with the user idToken you previously obtained.

Access Control

This project uses custom claims on Firebase Authentication to define which routes the users have access to.

Define custom claims to a user

This can be done in the server like below:

await admin.auth().setCustomUserClaims(user.uid, {
    storeOwner: true,
    buyer: false,
    admin: false
});

Configuring the routes

You can set a param (array of strings) on the httpServer.<method> function, like:

httpServer.get (
    '/product/:productId/full-details', 
    this.getProductByIdFull.bind(this), ['storeOwner']
);

In the example above, only users with the storeOwner custom claim will have access to the /product/:productId/full-details path.

Is this enough? Not always, so let's check the next section Errors and permissions.

API Errors and permissions

You can easily send an HTTP response with code between 400 and 500 to the client by simply throwing a new HttpResponseError(...) on your controller, service or repository, for example:

throw new HttpResponseError(400, 'BAD_REQUEST', "Missing 'name' field on the body");

Sometimes defining roles isn't enough to ensure that a user can't access or modify a specific data, let's imagine if a store owner tries to get full details of a product he is not selling, like a product of another store owner, he still has access to the route because of his storeOwner custom claim, but an additional verification is needed.

if (product.storeOwnerUid != req.auth!.uid) {
    throw new HttpResponseError(
        403, 
        'FORBIDDEN', 
        `You aren't the correct storeOwner`
    );
}

🚫 Permission errors

  • "Only storeOwner can perform this operation"

Means you are not logged in with a user that has the buyer claim rather than with a user that contains the storeOwner claim.

  • "You aren't the correct storeOwner"

Means you are logged in with the correct claim, but you are trying to read other storeOwner's data.

  • "Only admin can perform this operation"

Means that this operation requires to be logged with a user that has the admin claim, but the current user hasn't.

  • "Requires authentication"

If you forget to add the Authentication header

Authentication fields on Express Request Handler

This project adds 3 new fields to the request object on the express request handler, you can also customize this on src/api/@types/express.d.ts TypeScript file.

req.authenticated

type: boolean

Is true only if the client is authenticated, which means, the client informed Authorization on the headers, and these values were successfully validated.

req.auth

type: UserRecord | null

If authenticated: Contains user data of Firebase Authentication.

req.token

type: DecodedIdToken | null

If authenticated: Contains token data of Firebase Authentication.

Getting in touch

Feel free to open a GitHub issue about:

  • ❔ questions

  • 💡 suggestions

  • 🐜 potential bugs

License

MIT

Reference

This project used as reference part of the structure of the GitHub project node-typescript-restify. Thank you developer!