Securing GraphQL queries and mutations

Good afternoon, everyone.

1 - Does anyone know how to restrict GraphQL queries and mutations so that only a specific app can execute them, or so that the user must be authenticated?
Example: if I access the /_v/private/graphql/v1 endpoint in Postman and run a query, the data is returned. Ideally, that query should be restricted from external environments, so that when accessed via Postman, for instance, a 403 is returned.
I tried using Role-based policies: Policies but regardless of the rules I set in the resources, I can always execute the query in Postman and get the data back.
I’m not sure whether those policies actually work for this purpose.

2 - Has anyone implemented rate limiting on VTEX’s Node runtime, or do you know if that’s even possible?

Thanks.

Good morning JoĂŁo
Regarding point 1, role-based policies work for blocking GraphQL calls; if you want to block routes, you would need to block them using Resource-based policies in the service.json, which is explained in that same document you sent, just further down

Regarding point 2, I never got around to implementing the rate limit, but there are libraries that can be used to implement that rate limit

Thank you, Ana.

“Regarding point 1, role-based policies work to block GraphQL calls”
What exactly does ‘blocking GraphQL calls’ mean? What is the expected behavior?

Regarding rate limiting, libraries always ask you to add them, for example, inside an app.use(), before the routes, etc. Since the VTEX infrastructure is a bit different, I haven’t been able to find any that fits this scenario. Do you know of any?

Thank you again.

Good morning JoĂŁo
The idea would be to block the search directly in GraphQL, like the GraphQL IDE or calls in React.

Regarding rate limiting, there are 2 approaches I’ve seen people using: one is using a function from vtex/api — there’s this example here — and another using sleep

Good morning, Ana.

Thank you once again.

I tried using this example, but it doesn’t seem to be working.
The function executes, but tokensLeft is always the same regardless of how many requests I make. I believe it should be decremented every time a new request is made.
I ran over 2,000 requests with a 1ms interval and there was no rate limiting at all.

Code:

import { perMinuteRateLimiter } from "@vtex/api/lib/service/worker/runtime/http/middlewares/rateLimit";
import { createTokenBucket } from "@vtex/api/lib/service/worker/runtime/utils/tokenBucket";

const teste = async (_obj: any, args: any, ctx: Context, _info: any) => {
    const { clients } = ctx;

    const globalLimiter = createTokenBucket(2 * 1000);
    console.log(
        " ~ file: _teste.ts:8 ~ teste~ globalLimiter:",
        globalLimiter
    );

    ctx.set("Cache-Control", "no-cache, no-store");

    const request = await clients.teste.teste(args.email);

    perMinuteRateLimiter(2 * 400, globalLimiter);
    return request;
};

export { teste};

Response on each request:

09:35:09.852 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674110378,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3
09:35:10.097 - info: [12:35:10.626Z] [95] ***/routesapi:__graphql 200 POST /**/routesapi/_v/graphql 268 ms service-node@6.36.3
09:35:11.743 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674112270,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3
09:35:11.802 - info: [12:35:12.329Z] [95] ***/routesapi:__graphql 200 POST /**/routesapi/_v/graphql 60 ms service-node@6.36.3
09:35:13.465 - info: ![🚀](https://fonts.gstatic.com/s/e/notoemoji/15.0/1f680/32.png) ~ file: _teste.ts:8 ~ teste ~ globalLimiter: TokenBucket {
loadSaved: [Function],
save: [Function],
removeTokensSync: [Function],
removeTokens: [Function],
size: 1000,
tokensToAddPerInterval: 2000,
interval: 60000,
tokensLeft: 1000,
lastFill: 1677674113975,
spread: true,
parentBucket: undefined,
maxWait: undefined
} service-node@6.36.3

I’m trying to look into this through tickets, but if you have any ideas or suggestions, I’d appreciate it.

Thank you!!

What rate limit would you like?

To run the tests and confirm that everything is working, the ideal would be between 2 and 5 requests for the rate limit.

I found an example here for testing
Maybe it’ll help you:

describe('Rate limit per minute', () => {

  test('Test per minute middleware', async () => {
    // 2 * 1000 because bucket size is / 2 in order to not overflow amount of requests in 1 minute
    const globalLimiter: TokenBucket | undefined = createTokenBucket(2 * 1000)
    const perMinuteRateLimiterMiddleware = perMinuteRateLimiter(2 * 400, globalLimiter)

    await expect(
      execRequests(300, perMinuteRateLimiterMiddleware)
    ).resolves.not.toThrowError(TooManyRequestsError)

    await expect(
      execRequests(300, perMinuteRateLimiterMiddleware)
    ).rejects.toThrowError(TooManyRequestsError)
  })
})

Thank you Ana.

I just couldn’t find execRequests. Is it some library import? A function?

It’s a function!
I forgot to include it, sorry

type Middleware = (ctx: ServiceContext, next: () => Promise<any>) => Promise<void>

async function execRequests(requestsAmount: number, middleware: Middleware) {
  await Promise.all(
    new Array(requestsAmount).fill(null).map(_ => middleware(nopCtx, nopNext))
  )
}

Good afternoon JoĂŁo.
While looking for this function, I think I found another implementation approach that should solve your problem.
Leaving it here for you!

import TokenBucket from 'tokenbucket'

export function createTokenBucket(rateLimit?: number, globalRateTokenBucket?: TokenBucket){
  return rateLimit ? new TokenBucket({
    interval: 'minute',
    parentBucket: globalRateTokenBucket,
    size: Math.ceil(rateLimit / 2.0),
    spread: true,
    tokensToAddPerInterval: rateLimit,
  }) : globalRateTokenBucket!
}

export function perMinuteRateLimiter(rateLimit?: number, globalLimiter?: TokenBucket) {
  if (!rateLimit && !globalLimiter) {
    return noopMiddleware
  }

  const tokenBucket: TokenBucket = createTokenBucket(rateLimit, globalLimiter)

  return function perMinuteRateMiddleware(ctx: ServiceContext, next: () => Promise<void>) {
    if (!tokenBucket.removeTokensSync(1)) {
      throw new TooManyRequestsError(responseMessagePerMinute)
    }
    return next()
  } 
}

Just a reminder that you’ll need to import the tokenbucket library to be able to use these functions!

Good morning, Ana.

Thank you for your help.

I believe I understood why the tokensLeft aren’t being subtracted every time the request is made.
Every time my code runs, it defines the createBucket and the perMinuteRateLimiter with their respective values, meaning it always runs with the same values. The right approach would be to store these values in memory, for example — that way, every time the code runs, the memory space would already read that value and not redefine it. This way, the function to remove tokens would keep subtracting from that value in memory.

I don’t know if the VTEX library is doing this under the hood, but it doesn’t seem like it.