The principles of client-server communication
Are you new to backend? Do you want to learn how to authenticate users properly? This post will not go over any of the specfics, but it will describe the general principles of client-server communication you need to understand to successfully write a backend and connect it to your web users. These principles do not just cover the web — they cover any form of client-server communication — but that will be the main focus here.
Roughly speaking, there are two things you need to know:
- The server has no “memory” of you, it cannot recognize if two requests come from the same person/machine/etc. (They can technically use your IP but that’s not reliable for authentication — consider a public library!) Thus the client must authenticate for each request.
- Anyone can be a client and send an arbitrary request for your server, so you must verify all requests.
Either way it boils down to the same thing: verify all your requests and make sure you make no assumptions about them when handling them server-side. In particular, this means do not rely on any client-side form controls: Inspect Element or just curl can trivially disable them. Client-side checks exist for convenience, not security.
Authenticate for each request
The client will make separate requests for each distinct action it
wants to take with the server. For instance, if I wanted to write a new
topic in a forum that would be a POST request (POST requests are
generally used for write operations to the server’s database). If I
wanted to then reply to that same topic, it would be a separate POST
request. And if I wanted to read said topic and reply later, that would
be a GET request (generally used for read-only operations). Each action
the client takes is one request. (This applies for loading a
page too: when you go to dennisc.net, you’re making a GET request to the
route /
.)
To protect against malicious requests the client has to prove within each request, via means of a cookie or a token, that they are who they say they are. This is authentication.
There are two common ways to authenticate on the web that we will cover: username/password and session tokens.. To simplify, a website usually works as thus:
- use your username/password combination to generate a session token and set it as a cookie (this is what happens when you “log in”)
- for any other GET or POST requests, the client should send the session token to verify its identity.
The browser automatically sends the session token to verify its identity by storing it in a cookie, which it then automatically sends on every request. This is what cookies are: just information the browser sends on your behalf for every request to a website.
Anyone can make an arbitrary request
Whenever people first try to write a full-stack web “app”1, they inevitably fall into the trap of thinking that only officially approved clients (i.e. only the frontend of your website, and not any external actors malicious or otherwise) will send requests to your server. This leads to embarrassing things like “authenticating” requests based on username rather than something verifiable like a session ID.2
Personally, downloading Postman has been really informative. Not only is it possible, it is also easy for any user to send whatever they want to your endpoints. cURL should be similarly enlightening.
In any case, you can either only allow your website to act as a client (this is done for forms using a CSRF token, but you should not solely rely on this) or accept that clients can send whatever they want. This also means that any clientside checks in formdata must also be done serverside.
Summary
The server has no memory of clients, so they must send identifying information on each request. Anyone can act as a client and they can send anything, and you must verify the validity of each request server-side.
Basically, assume every request is made with malicious intent until they prove otherwise.3
Scarequotes because most of the time you don’t need client-side interactivity besides what HTML and CSS provide, which is a lot.↩︎
This mistake is a lot easier to make if you use a Javascript framework because then you have the ability to determine identity client-side. If your only method of determining identity is server-side, you won’t make the mistake of trusting client-side. Another reason not to! I’ve done it, and so has the FBI.↩︎
Obviously this only applies for write or confidential read operations; if a client is request a public static page, you can serve it to them without checking their identity.↩︎