Does dbAuth use JSON Web Token or database session?

Does dbAuth use JSON Web Token or database session?

It uses an encrypted cookie and each request is validated towards the database, so I would say more towards the database session.
Docs: Authentication - dbAuth - How It Works

1 Like

So, the backend is hitting the DB to validate the session cookie/token in every request?
Is it possible to use a cache to avoid hitting the DB?

The dbAuth code runs in a serverless function, so your implementation for caching a client session cookie would be provider-specific. Netlify and Vercel are both abstractions over AWS Lambda, but place restrictions on what other native AWS services you have access to. Given that:

AWS Lambda will generally terminate functions after 45–60 mins of inactivity, although idle functions can sometimes be terminated a lot earlier to free up resources needed by other customers.

In the simplest caching strategy, you could just use a local variable in your Lambda’s execution environment (e.g. outside of your handler code). Lambdas reuse the same execution environment for the second request hitting the function unless the load of incoming requests means AWS’s algorithm determines it should spawn additional Lambdas for concurrent processing.

If your project is busy enough that it’s often spawning multiple Lambdas (so a local variable isn’t available across them), you could use AWS S3 buckets or Redis if you want to avoid the database call. Netlify has an S3 integration. With this architecture you lose the ability to (easily) invalidate sessions.

A single DB query is so efficient that I personally wouldn’t worry about it. PostgreSQL (or AWS RDS or whatever PostgreSQL-aaS you’re using) caches query plans and the table data is likely still in RAM, so there’s little processing happening on the DB server.

1 Like

Hi Kevin, thanks for clarifying this. If I go with a baremetal deployment, using a local cache (like node-cache for example) will reduce the hits to DB right? I’m assuming that, in baremetal, a serverless function is just a regular JS function and a global cache variable will persist between different requests. Is that correct?

Thanks in advance.
f

If I had a baremetal environment to do what you’re trying to do, I’d probably write the code as a small Express app and add a middleware for caching using node-cache. The APIs for serverless functions and Express middlewares are different.

In a serverless function, you get as parameters an event object (which varies depending on how the serverless Lambda is called) and a context object (which has metadata about the current environment). You also get a callback, but you can write the Lambda as an async function and avoid handling it.

In an Express middleware, you get a request object, a response object, and a callback that you need to call usually named next.

You could create a persistent cache between requests like this using Express and node-cache.

You can run Lambdas baremetal, but the tooling around it is intended for testing serverless functions. I probably wouldn’t use them in production. Other solutions to run serverless functions on baremetal are more complex, like setting up an OpenStack cloud environment.

Internally dbAuth caches the response for 5 seconds to avoid making it multiple times on a single page load, but we could maybe make that configurable? The idea is that you want every request to verify with the server that the user is who they say they are so you’re not vulnerable to session reply attacks and the like.

Another concern when introducing a cache is that you’re delaying how long a user still has access to your site if you ban or lock them. Some of the JST auth options we have may not need a refresh token for 4 to 6 hours. That means a banned user can still do whatever they want for that amount of time before a refresh token is requested and the server realizes they should now be locked out, and deny further requests. (You could mitigate this by double-checking on any sensitive action that the user is still valid, but you’re making a lot of additional work for yourself.)

Are you seeing that the dbAuth check for the user is introducing a noticeable delay? On my current app that auth?method=getToken returns in 25ms in production.

1 Like

Hi @rob, thanks for taking your time on this.

Are you seeing that the dbAuth check for the user is introducing a noticeable delay?

Nope, I worked on my first Redwood project using Auth0 without almost any issue except a wired behavior explained here: Auth0 iframe in unauthenticated routes

For my second project, I decided to use dbAuth. After setting up, I found this code:

export const getCurrentUser = async (session: Decoded) => {
  if (!session || typeof session.id !== 'number') {
    throw new Error('Invalid session')
  }

  return await db.user.findUnique({
      where: { id: session.id },
      select: { id: true, email: true },
  })
}

And I wondered if it’s possible to avoid hitting the DB with a simple cache, something like this:


import NodeCache from 'node-cache'
...

const cache = new NodeCache()

export const getCurrentUser = async (session: Decoded) => {
  if (!session || typeof session.id !== 'number') {
    throw new Error('Invalid session')
  }

  let user = cache.get(session.id)
  if (!user) {
    user = await db.user.findUnique({
      where: { id: session.id },
      select: { id: true, email: true },
    })
    cache.set(session.id, user)
  }
  return user
}

Testing this in my dev local environment, seems to work.
The only issue I don’t know how to resolve is: how to remove the user from cache after logout?
Redwood has handlers for:

  • login
  • signup
  • forgotPassword
  • resetPassword

but there is no handler for logout, that’s a pitty.

You could mitigate this by double-checking on any sensitive action that the user is still valid, but you’re making a lot of additional work for yourself.

Of course, with a cache we have to manage to keep all things synchronized. This should not be a problem if we have a “logout hook” the remove users from cache when they close their session. I’m not sure what you mean with “lot of additional work”, I think is not so difficult to do this.

Finally, given that auth is implemented using RW functions, I’m not sure how this may change if we deploy using AWS lambdas or in a baremetal environment. Probably, there is no problem if we use an external cache server like Redis, but using NodeCache may not work with serveless functions.

Internally there’s a handler for the logout action, but you’re right, there’s no user-facing handler in auth.js. That could be a handy addition! If you’d like to add it we’d be happy to review a PR!

Here’s where logout is handled on the api side: redwood/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts at main · redwoodjs/redwood · GitHub

And here’s an example of calling the user-defined handler: redwood/packages/auth-providers/dbAuth/api/src/DbAuthHandler.ts at main · redwoodjs/redwood · GitHub

I’m not sure what you mean with “lot of additional work”, I think is not so difficult to do this.

I meant that if you cached the user’s login detail, and then banned that user, you would need to check in each senstive action (like creating a new record) that the user was actually still valid (not banned) and allowed to make that change. But if you are manually managing the cache you could always invalidate their cached credentials as soon as you ban them!

Just wanted to throw in a couple of thoughts.

The DB hit you’re referring to is unlikely to be a full hit. It will query on a primary key, which is “very highly” likely to already be cached within Postgres’ own caching mechanisms as well as the OS cache.

In other words, it’s not only unlikely that any other caching mechanism would outperform PGs internal caches, but it also won’t make a notable difference on DB performance overall.

The query would have to be more complex or output a LOT more data for it to be worth the complexity of caching that query.

1 Like

Another thing I just thought of @fmiranda is that you should be careful about doing optimizations around authentication.

There’s an area of security known as “timing attacks”. This is where an attacker can determine from the time it takes for a request to complete because there are different paths in your code base that have drastically different response times.

The lesson in all of this, is don’t optimize early based on “it seems like this could be faster”. Often times there are real reasons to not simply make something faster, optimization always has trade offs, and is only worth it in very specific cases.