Do I need a backend API between FaunaDB and my app? What are the use cases of an API?

Hello.

I noticed some front-end apps just query fauna directly without querying an api at all.
How good this is? Can someone bring use cases that I’ll probably need an api in the middle?

Tks.

3 Likes

I started with full API then I realised I don’t need it for almost all cases except those which has to be rate-limited because they are not protected by auth, eg. login, sign up, etc.

Using Fauna directly from browser makes your app really fast, 48ms for queries is super awesome. To not bloat client code, I am using UDFs.

I am still using middle API, but only for rare cases, like queues, email sending, etc.

3 Likes

Use cases for using an API in between FaunaDB and your app

  • httpOnly cookies which adds security (see later on whether it’s necessary or not)
  • Extra monitoring
  • If you want something like IP-based rate-limiting
  • If you’d like to implement some logic in your backend (although most if not all logic can be implemented in FQL and it’s imho a good idea to have your logic close to the data (some people don’t like that idea).
  • Being in control, some programmers just want more control and see everything passing their servers.

Clarification on the security implications of httpOnly

Since I mentioned httpOnly cookies, it makes sense that we also explain the extra advantage that brings and the different approaches. There are many ways to store keys in memory.

  • local storage
    vulnerable to XSS

  • regular cookies
    vulnerable to CSRF and cookies could be read by the client making them also vulnerable by XSS. It’s much more nuanced than that though, a good explanation on this.

  • in memory (in a JavaScript variable)
    vulnerable to XSS … but this is actually safer than localstorage and regular cookies, and that security is indeed security by obfuscation which is questionable (yet often still a good idea). The idea here is that an automatic attack that briefly gets in and quickly copies over your cookies or localstorage content will not have any success. If it’s a manual attack he’ll have to do a significant effort before he finds an in-memory secret compared to localstorage or cookies which are, of course, easy to find. However, in memory is annoying since it logs you out on each refresh!

  • httpOnly cookies
    Protected from XSS, vulnerable to CSRF (but there are other ways to protect against that)
    Cookies that can’t be read from JavaScript (if your browser supports it and handles it correctly, there are always some subtle security things you have to know). These require a backend for the endpoints where you want to use the data that is stored in the cookie. Which is logical since the whole point is that JS can’t access this data.

Option 1: full backend with edge serverless

HttpOnly cookies are your best bet but it indeed requires a backend and that costs latency and/or you have to set up this backend multi-region since you don’t want to lose the multi-region and low latency aspects of FaunaDB. You can do a full API with Cloudflare Workers, (check the edge ones here) for example which would add only 10-20ms.

*** > Example incoming (still needs a bit of time though)!***

Option 2: backend for only auth

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.

*** > Example incoming!***

Option 3: all in memory.

You will require your user to log in again on each refresh and should not make the tokens too long-lived. I would personally not advice this approach if you are building a serious production app. It’s also not great from a UX perspective that the user has to log in all the time (which is not an issue in option 2)

*** > Example incoming!***

What code could potentially access my in-memory secret?

Malicious code, injected in your app (XSS)

The real potential harm when storing secrets in JavaScript memory would come from malicious code that was somehow injected in your application (via injection, via a malicious library that was included), basically XSS, if you are interested in the prevention rules, check this.

Browser Extensions? Normally not

I am definitely not a security expert but the answer is more subtle. As far as I know, extensions do not get access to your app code.

A malicious extension could (if it has those permissions) however monitor your dom so it would never be a good idea to store that secret in your dom.

Make sure to verify other sources about such security questions. According to Google Chrome’s information:

Link on extensions and isolation
“Content scripts live in an isolated world, allowing a content script to makes changes to its JavaScript environment without conflicting with the page or additional content scripts.”

IFrames? In theory not a risk

Iframes are protected via the same-origin policy. Iframes can access the parent window via window.parent if they are on the same domain (which hopefully is not the case for an iframe with malicious code, else you have a bigger problem)

Fun fact is that you can even use iFrames like this to your advantage to secure frontend variables: https://pragmaticwebsecurity.com/files/cheatsheets/browsersecrets.pdf
It’s relatively complex to implement and get right though.

Link on iframes and cross-window communication
“otherwise, if it comes from another origin, then we can’t access the content of that window: variables, document, anything. The only exception is location : we can change it (thus redirecting the user). But we cannot read location (so we can’t see where the user is now, no information leak).”

!!! Feel free to correct me or ask further questions, security is important and understanding the implications should be a collaborative effort !!!

8 Likes

Cookies are definitely more secure than localStorage.

I’m not a security expert by any means but my understanding is that with these options the only way to steal a cookie would be with physical access to the machine:

  • httpOnly: JavaScript can’t access the cookie.
  • sameSite: The browser will only send the cookie to the same domain that set the cookie. This prevents CSRF attacks which basically consist in making a request to a third party site and read the cookie on that request.
  • secure: cookies are only sent via HTTPS preventing man-in-the-middle type of attacks.

Unfortunately this means that at least some part of the logic has to live in a server.

Another point to be mentioned is that, depending on your use case, if you query Fauna from your clients you might need some business logic in each of your clients. This logic would have to be maintained across different platforms (web, iOS, Android, C#, etc). I guess this can be alleviated by using custom functions but personally in that situation I’d rather keep my clients as agnostic as possible and just use an API.

2 Likes

Great addition @pier, things I should have added.
One addition though. We shouldn’t say that cookies are more secure than localstorage. I’m sure that you are aware but for people not being aware of it reading this. Each have diffferent vulnerabilities. If you take the right measures though you can get achieve a higher level of security with cookies.

  • Cookies open you up to CSRF attacks, but other measures can be taken to protect you against CSRF. These attacks are only relevant if you do updates (e.g.making money transfers). In the case where we use cookies in a partial backend (only auth) the worst that can happen is that someone refreshes the token on your behalf (but he will not get access to it!). If you use cookies and have everything behind a backend and use cookies for every call, be vigilant and protect yourself against CSRF.
  • Both Localstorage as Cookies can be accessed from JavaScript which makes them vulnerable for XSS. Setting httpOnly prevents that.

Exactly, but as I mentioned this is avoided by using the sameSite cookie option.

The link you posted also mentions this:

If the user is logged in to the vulnerable web site, their browser will automatically include their session cookie in the request (assuming SameSite cookies are not being used).

FYI Chromium will only accept cookies without the sameSite attribute over HTTPS in an upcoming version.

In contrast any JavaScript running in your application can access LocalStorage.

This wouldn’t be too bad if one had total control over the JavaScript being executed, but in this day and age it’s very common to use dozens if not hundreds of NPM dependencies client-side. And we’ve seen hacked NPM packages mining bitcoins for example.

I agree this is rare though and it will greatly depend on the use case.

1 Like

Good points, in some special cases of applications it might even happen with sameSite if I’m not mistaking, these kind of apps are rare though. E.g. the type of applications where users can customize pages (with HTML) which is not decently sanitised and share this with other users via a share-link. They could trick someone else in deleting all their data on a different account (abusing their cookie) , I’ve seen that specific vulnerability in a few apps iirc :slight_smile:.

I would definitely avoid local storage as well personally and favor cookies. Just wanted to make sure users also consider the dangerous parts. Interesting discussion in each case :smiley:

1 Like

Good point I totally forgot to mention that!

Non sanitized user input is actually even more dangerous with LocalStorage as cookies can be protected from JavaScript with the httpOnly option.

I have built 2 examples covering options 1 (“full backend with edge serverless”) and 2 (“backend for only auth”) as described by @databrecht.

I did so using Fauna+Faugra and Next.js.

Note: the “edge serverless” part is ready, but it’s up to the developer how they will deploy the next.js api folder.

2 Likes

I’m setting up my backend api and I just wanted to confirm something. If you send an access token with permissions to the client, that client could circumvent your api and just use the faunadb directly, right? In order to prevent this, you’ll need to give an access token with no permissions (basically just for identity purposes), and your api will need to do two requests to faunadb. One for verifying the token and getting a temporary token for the request, and a second request for the data using the new token. Is this right?

Correct but requires some nuance probably :upside_down_face:.

  • A token indeed provides access, Fauna can be called from your backend as well as from your frontend. All you need is the token. The client will therefore be able to ‘circumvent’ your backend. That is often actually desirable. For some data you want to avoid that extra hop (as long as you don’t implement extra rate-limiting or logging on your backend that is required). Imagine you have an app where you can create public share-links for a certain resource and such a sharelink might gain a massive burst of traffic (I actually experienced such a case during elections). In that case contacting Fauna directly from the client makes perfect sense, you don’t have to make sure that your backend can handle that burst.

Your solution would work and allows you to use the Login functionality in Fauna. I would advise to store a property on your token to identify that it’s a different type of token (and verify that with ‘CurrentToken()’.

However, it might be a bit overkill to contact Fauna twice. Instead, you could make the login flow pass through the backend and store a token with actual permissions in a secure and httpOnly cookie. If you do it right, then the client should not be able to get that token and therefore is not able to circumvent your backend.

@databrecht thank you for your response. You’re a wealth of knowledge and your blog posts have been my main resources for the past few weeks.

For our specific use-case, we rely heavily on third-party applications to access our database, as opposed to our own applications (we’re basically the backend and payment processor). The applications go through an OAuth flow (client, code, pkce) which does implement your recommended secure and httponly cookie. However, at the end of the flow those applications expect an access_token and possibly a refresh_token.

I like your advice of having a specific property on the token to distinguish it. I’ve been thinking that the instance ref can be a document in a collection like ‘access_token’ that has the individual scopes granted by the user during the authorize flow or by set by the application at signup for the client flow. I’ll then have to do all the mappings with CurrentToken().

Just thinking out loud here :wink:

You don’t need to send the FaunaDB secret in plaintext within the Cookie, to the user :slight_smile: Check this example https://github.com/magiclabs/example-nextjs-faunadb-todomvc/blob/d091901a72a608648b699a08cadcabc6b49617c1/pages/api/login.js

You can see that the FaunaDB secret is obtained via:

const token = await userModel.obtainFaunaDBToken(user);

The following code encrypts the user data (including the FaunaDB secret) with Iron (GitHub - hapijs/iron: Encapsulated tokens (encrypted and mac'ed objects)), and creates a new cookie with the encrypted user object:

await createSession(res, { token, email, issuer })

Iron Code: https://github.com/magiclabs/example-nextjs-faunadb-todomvc/blob/d091901a72a608648b699a08cadcabc6b49617c1/lib/iron.js

So to sum up, you would send a hashed user object in your cookie, that can be decrypted on your backend (to retrieve the secret), right before you make any requests to FaunaDB :slight_smile: This makes it so you don’t need to trust any users with FaunaDB secrets, and they can’t bypass your API, directly to FaunaDB’s endpoint.

Note: You still want to setup ABAC permissions as normal, but this makes it so user’s can’t send requests directly to FaunaDB with their secret :slight_smile:

1 Like

I think what you are mentioning here is an alternative approach (and the only one available before we had CurrentToken()) where you would use separate type of documents (e.g. separate collections) for separate types of tokens. That’s definitely a good solution as well and keeps the different tokens much more isolated which is less prone to error. It does require you to create a document + token for each type of token you want to create. To be clear:

Approach one Attribute on token:

One user/account document with multiple types of tokens. All token refs would point to the user/account document (via the instance property). You would write roles by relying on the property from CurrentToken() in membership or directly in the roles. Or even directly in a UDF if required.

Approach multiple tokens:

One user/account document
plus a specific document (e.g. a doc on a collection user_access_tokens) for each type of tokens. All tokens refer to their own separate document (via instance).

It’s a cleaner separation arguably but the complexity is going to be higher since you will often have to get the user again for that specific token which involves some extra indirection.

Dmitry is absolutely right and has good points, if you want to be 100% sure that a client does not access Fauna directly, encrypt it, (everything has flaws, even httpOnly cookies have had problems in the past). The trade-off is of course the extra decryption step but that should be negligible. You could also opt to not store the fauna secret in the cookie and simply store a session id. That does require an extra step to fetch your Fauna token in case you want to make use of ABAC which may not be negligible (except if cached, I can imagine Cloudflare’s KV would be an excellent candidate for such a cache) but is common practice in many backend frameworks.

Thanks, that warms my heart. I do have to apologize as well to the community, the authentication series that I was writing are way overdue, but there are some necessary changes due to new features and other things have come up which took priority. They’re still coming up though!

1 Like

It’d be lovely to get a glimpse at this example :pray:

The old code can be found at the bottom of this thread:

I’m picking up the work again this week to finish the new code.

2 Likes

is there a sample for Option 2?

For anyone else who was looking for the blog post that @databrecht eventually published, I think this is it: Refreshing authentication tokens in FQL

1 Like

Why hasn’t anyone pointed out how unsecure this approach is for public applications.

  • Anyone can use your website, register, login, post things, etc. – including delinquent users who might be trying to inspect your app for vulnerability.
  • Everything you send to the frontend is visible to the receiver.

So the working principle here is FE -> DB API instead of FE -> BE -> DB. With FE -> BE -> DB your validations will most likely be in the FE and BE or just BE, but with FE -> DB API your validations are in the FE. This is all good and well especially if your app is internal (only available for your team) and you know for a fact that delinquent users will not be able to login. However, if that’s not the case and the delinquent user finds out about this, SHIT WILL HAPPEN, he can just use the console or any HTTP client to send malicious requests with unsanitized user input, that could include XSS, or empty fields where required, even skipping other things like for example, if you have a website where people can send chat messages to each other as long as they are friends, well now the delinquent user can just create those chat records right away skipping the validations.

The pattern FE -> DB API is what we use in our company with AWS Amplify but if I’m going to create a public app like Twitter, I’d use the FE -> BE -> DB pattern.

If each user has their own token with a set of predicated permissions, then in your scenario a delinquent user will log in, and… still just be able to post plain http to perform the same operations.

You still need to keep your users’ tokens safe, and this topic discusses some ways you can do that. But since each token contains a limited set of permissions, anyone who acquires that token will have limited access to the database. Unless you’re just handing out server keys to everyone which would definitely be inadvisable.

Another best practice is to wrap any operation a user can make into a UDF. That UDF can limit how much input is needed. For example, a CreateATodoForMe UDF which uses CurrentIdentity() to assign a relationship, rather than having the client use Create directly. A UDF can also add input validation, rate limiting, and a number of other features. Only give users tokens with permission to call the function can use it. And the Role’s predicate function can even limit certain users to calling a function with only certain parameters.

The blog post was an article about refreshing session tokens, not about architecting ABAC roles. If I hazard a guess, Brecht had the option to be concise about the refresh token workflow, or to add a bunch of additional boilerplate for Roles into the blog post.

I’m not saying security isn’t hard, nor that FE -> DB isn’t harder. And I’m definitely not offering a fool proof way to do it here. But I think it is possible to look at one problem at a time and find ways to mitigate it.