How to implement a refresh (httpOnly cookies)/access (in memory) token flow with a partial backend for auth?

Hey all,

I’ll start already explaining the most important part of the story for those people who have asked and who are eager to dive in themselves. I can’t write the full post yet though since I’m still missing a few things. A short recap of what I’m going to describe:

If only auth is behind a non multi-region approach then we actually might not care that much about the latency of the auth. If everything once you logged in runs smoothly. Here the idea is that we store the most sensitive data (the refresh token), in the httpOnly cookie and the token that can cause the least harm (because it’s shorter lived) in memory. You have to realise that if some malicious code is able to access your JavaScript variables, that you have a problem (this should never be the case)! This approach mitigates this problem if it does happen by making sure that the tokens that this code could access are short-lived. Meaning that the harm they can do is limited in time. The most powerful token (refresh token) that could give you more long-term access can’t be accessed from the frontend.

  • short-lived token in memory Make a very short-lived token that you will use to access FaunaDB from JavaScript (stored in memory)
  • refresh token in httpOnly cookie Make a refresh token that is stored in an ecrypted httpOnly cookie which you will use to refresh the access token once it’s too old or when a user refreshes. Make sure to also refresh the refresh token since it adds some security e.g. if you get two times the same security token, you know that someone stole it. It’s good to notice that CSRF is less of an issue since you won’t use the cookie for manipulations of your data.

Disclaimer

I’m writing this out fast here since it’s a question that is asked often. Please verify yourself as well whether everything makes sense in your case, security shouldn’t be taken lightly :slight_smile:. Feel free to deliver feedback.

Backend

The idea is to have a backend that is able to set httpOnly cookies, but only for login/register/refresh/logout calls. Essentially, once you have logged in, you can continue and use your frontend (short-lived) token to directly access FaunaDB.

I would personally use UDFs as well to implement each of these calls and a bootstrap token, just like in the frontend-only approach. However, in this case, the key that can call these functions (which I’d call the bootstrap token) will be used by the backend.

In this case, this is a node app (I’ll convert those to Zeit/Netlify examples later on) in which the cookie settings are defined as follows:

Note that you should set secure to true in a production environment and foresee the proper certificates.

Frontend calls

This means that all frontend auth calls will basically be calls to the backend.

Other calls, will be straight to FaunaDB with the access token.
Similar to before, we start with a client. However, we start in this case with a client that can absolutely do nothing (it might as well be client = null) .

Once we get an access token back from the backend, we can actually do calls to FaunaDB.
What is different? This access token can be much shorter-lived (it would be inconvenient to log in all the time in the frontend-only case) since we can do a silent login via the refresh token and/or on a refresh we can also retrieve a new access token to continue working, e.g. refreshing the page won’t log you out.

How to implement the token logic.

There are many ways to do that, but the most important thing is that you have short-lived token in the frontend. This is the reason why the example is not out there yet, although you can implement that already yourself, we are going to make it easier.

Assume that ttl on tokens is perfectly respected than we can take this approach:

Access tokens

We have Accounts (or Users), as usual. Access tokens are regular FaunaDB tokens with a ttl on those documents. We set the ttl to be 10 minutes for example.

Refresh tokens

Refresh tokens are ideally on a second collection which I call ‘sessions’ to nicely have different roles for refresh tokens vs access tokes. The only thing I would allow it to do is provide a new access token for the same account as the previous access token, and have the necessary permissions to delete tokens in case of logout (or in case we detect some anomaly (e.g. two times same refresh token used). For example, these session documents could look as follows.

{
  "ref": Ref(Collection("session"), "269138757140087303"),
  "ts": 1592929576040000,
  "data": {
    "account": Ref(Collection("accounts"), "269138414854472198"),
  }
}

They are linked to the account for which they will act.
The actual ‘refresh token’ will be a token for the session document.

Creation of access/refresh tokens

The creation could then look like this:

Logging in

When we look at the login logic (this is the FQL that will be put into a UDF function), it could look like this. Basically we are going to use Identify instead of Login to verify the Identity. And if it’s valid we’ll create both an access token as the session as the refresh token on that session.

And then it’s of course important how we store it. Access token is sent to the frontend.
Refresh token is stored in the httpOnly cookie.

Refresh

On a refresh of the page, you can then verify whether there is a refresh token. If there is… use it in your client (assume it’s privileges) and call the logic to refresh the token (‘refresh_token’)

That logic is essentially going to generate a new access token and/or access token.
In that part, there are many choices to make… hence why I have to write it out decently. You could try to detect whether a refresh token is used twice, but you have to be careful about multiple tabs. You could let your refresh token live forever. You can chose to revoke the access token in a refresh, but you again have to be careful about multiple tabs. Another solution is not to revoke access tokens in a refresh at all… under the assumption that you don’t care since they are short-lived.

I’ll try to write the different options out and provide a full example.

8 Likes