18

I am using Firebase to develop an app that uses Cloud Functions as a REST API internally. My question is, is there an easy way to implement per-IP/per-user rate-limiting similar to what slack uses, except on a per-IP and per-user basis, rather than per-app (since it's all one app). Optional support for small bursts is preferable as well.

Example code (see the // TODO: comments):

exports.myCoolFunction = functions.https.onRequest((req, res) => {
        // TODO: implement IP rate-limiting here
        unpackToken(req).then((token) => { // unpackToken resolves with a response similar to verifyIdToken based on the "Authorization" header contents
                // TODO: implement user-based rate-limiting here (based on token.uid)
                if (!req.body) return res.status(400).end();
                if (typeof req.body.name !== "string") return res.status(400).end();
                if (typeof req.body.user !== "string") return res.status(400).end();

                // more input sanitization and function logic here

                return res.status(501).end(); // fallback in all requests, do not remove
        }).catch(() => res.status(403).end());
});

I want to terminate the request simply with a 529 Too Many Requests status code if the rate limit is exceeded. This is to prevent application errors from flooding the network and to prevent abuse of the REST API.

This should take into account Firebase spinning up/down server instances and having multiple instances running simultaneously.

I am also using a Firestore database and can use the legacy real-time database if necessary.

Community
  • 1
  • 1
Potassium Ion
  • 2,075
  • 1
  • 22
  • 39

2 Answers2

15

I made a library for rate-limiting calls to firebase functions: firebase-functions-rate-limiter The library uses realtimeDB or firestore (configurable) as a backend. It stores data, in a similar approach that Frank described, but is more economical. Instead of using a collection, it uses a single document with array per each qualifier (eg. a user id). That means there is only a single read for an exceeded call, and a read-write for an allowed call.

$ npm i --save firebase-functions-rate-limiter

Here is an example:

import * as admin from "firebase-admin";
import * as functions from "firebase-functions";
import { FirebaseFunctionsRateLimiter } from "firebase-functions-rate-limiter";

admin.initializeApp(functions.config().firebase);
const database = admin.database();

const limiter = FirebaseFunctionsRateLimiter.withRealtimeDbBackend(
    {
        name: "rate_limiter_collection",
        maxCalls: 2,
        periodSeconds: 15,
    },
    database,
);
exports.testRateLimiter = 
  functions.https.onRequest(async (req, res) => {
    await limiter.rejectOnQuotaExceeded(); // will throw HttpsException with proper warning

    res.send("Function called");
});
jblew
  • 506
  • 3
  • 9
  • Can the period of second be 30 days? I want to make an API with monthly subscription. And is it possible to run several separate functions simultaneously? So can this script create a database/entry that counts the call for every single function seperatly? – Siem Peters Mar 18 '20 at 17:31
  • 1
    Sure, it can be limitless. Just keep in mind to make it shorther than our world's lifespan (or integer max value) – jblew Apr 26 '20 at 12:07
  • @jblew if I choose Firestore to save that data limit, then there will be a limit of 1 Mb per document, will it be handled by this library or by ourself ? – sarah Sep 24 '20 at 01:55
12

Doing this on a per-user basis sounds fairly straightforward:

  1. Pass the ID token of the user to Cloud Functions with each request.
  2. Decode the ID token in your Cloud Function to determine the UID. For an example of these first two steps, see the functions-samples repo.
  3. Push the fact that user UID has called the function to a database, probably adding it to a list. E.g. admin.database().ref(`/userCalls/$uid`).push(ServerValue.TIMESTAMP).
  4. Query for the number of recent calls with something like admin.database().ref(`/userCalls/$uid`).orderByKey().startAt(Date.now()-60000).
  5. Count the results and reject if it is too high.

I'm not sure if the IP address of the caller is passed to Cloud Functions. If it is, you can do the same logic for the IP address. If it isn't passed, it'll be hard to rate limit in a way that can't be easily spoofed.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 1
    Does Firebase real-time database charge for read/write ops from cloud functions? – Potassium Ion Jun 07 '18 at 14:15
  • 1
    @PotassiumIon Yup – fionbio Dec 08 '18 at 14:58
  • 30
    I really wish this is provided by the infrastructure, since when you check UID and frequency, you are already handling the request. It also adds the CPU time, memory, and network bandwidth of your function, which can increase the cost (price) of running it. Rate limiting is more an Aspect that can be made independent of any Cloud Function. – lqu Jun 27 '19 at 20:10
  • 3
    Origin IP addresses are available within the request (req) object for anyone wondering. – pinglock Sep 19 '20 at 19:35