Our API is going to be relatively simple. We need to serve our HTML pages, handle login and the creation of users, and send and retrieve messages.
The first part of the API is ensuring that we have authenticated users.
Let's talk about authentication#
We have the ability to check a user's password to a hashed and salted version saved in the database. However, we don't want to have to do that on every request, because that would require the user to send their password to us every time they took any action. Doing that would either be incredibly cumbersome for the user, or very insecure, as we would have to store their password somewhere to send it on every request.
Instead, we will do what everyone else does, and set up a session for the user using cookies. We have a couple options here.
The first option is to set up session cookies for each user. This essentially gives the user a cookie with a unique ID that we can use to look up their information every time they make a request. This is a very common and sensible way to handle user authentication.
The only drawback is that we have to store that information somewhere, which centralizes the looking up of user information. For example we could create a
Session struct and store it in our database the same way we store users and messages. Then we would simply do a lookup every time a user makes a request.
Or we could go with a second option, using a cryptographically-signed cookie that includes user information in it. The primary benefit of generating this kind of cookie rather than a session token is that when a user makes a request, we don't need to look up their session information in the database or some other data store. We only need to verify that their signed token is valid, and then we know that the information it contains can be trusted, and we can use that.
Neither of these options is perfect.
For session tokens, you always have to look up the session information from a data store. Even if that data store is designed specifically for that kind of lookup, like the key-value store we wrote, it still involves a network request, and creates an additional point of failure.
Signed cookies containing data, on the other hand, don't require the additional request nor even the storage of session information. That may seem like a huge win until you consider that all the information in the session has to be stored somewhere, and that somewhere is in the cookie, meaning every request a user makes sends all of that data, regardless of how much or little of it you need. So although there is a somewhat significant advantage, it's not free.
In order to simplify our setup and not have to store and retrieve sessions, we are going to go with the second option, cryptographically-signed cookies. Specifically, we are going to use JSON Web Tokens, or JWTs. Going into exactly how JWTs work is beyond the scope of this book, but if you want to find out more you should check out jwt.io.
JWTs will require the use of a library to provide the necessary pieces for creating, validating and pulling information from tokens. Luckily, a library has been written to make it easier to use JWTs with
The main thing the library needs to work is a struct called
JWTAuth. Conveniently, all the functions we will use are defined from this struct, and we can use a pointer to it so we can use it in a number of places. This means for us, it's exactly like our database connection, so we should put it in the same place in
Now that we have that essential piece of information, let's go back to
crypto.go and define our methods for making the token and retrieving information from it.
The only thing we need to store in the token is the user's name. Since we won't be looking up any user information, we don't need to store their ID or any other information. Making the token will involve calling
Encode on our
JWTAuth that we've stored as
TokenAuth in our
Because the token is JSON, it accepts any kind of information in a map so long as there are strings for keys. In order to be descriptive, we will store this information as
"user_name", but we could have used any string for it.
The only remaining piece left to do is retrieve this user name from the token. For this, we actually won't need
TokenAuth directly, because the
jwtauth library provides a
chi middleware function that will store all the necessary information in the
context.Context which is included in the
http.Request. Unsurprisingly, it also provides a method to retrieve the token data directly from context. Using that, we can write a function to get the user name from the context.
jwtauth.FromContext method returns us the token itself (which we don't actually care about), the token data, and an error in case there was an issue retrieving the data. We are explicitly ignoring the error here, because we will already have validated the token by the time we call this function, so we can be sure it won't fail if it gets this far.
With those final
crypto.go functions done, we are ready to move on to the server itself.
The server routes#
We have all of the surrounding pieces ready, so now we can make our server and all of the handlers we're going to need.
The first step is to decide what routes we need our server to have, so let's enumerate them and their paths.
/- the login page
/login- the login handler
/newuser- the new user creation handler
/chat- the chat page
/api/messages- the get messages route
/api/messages- the send messages route, differentiated with the above by using POST requests
So in all there will be six routes. We also have to consider which of these need to be behind authentication and which don't. Luckily that is a pretty simple decision - users will need to be logged in so they can see the chat as well as send and receive messages. The other routes will not. So from the list above, our first three routes don't require authentication, and the last three do require authentication.
In a new file,
server.go, we can start to set everything up.
Most of this should look similar to what we have done before, but there are a couple of new things:
First, we're calling
Config so we have access to both our JWT library and our database. It also lets us pass that same information into all of our handlers that need the database without having to use a global variable or create a database connection in the handler.
The other new piece is the
r.Route. This is a helper method
chi provides that allows us to group methods that require similar functionality or path prefixes. We are using it twice here, once to set up authentication for all of our routes that need it, and a second time to group both of our messages endpoints, which we are putting under the
/api path prefix.
So this sets up
/chat along with either message route to have authentication, and the message routes' paths will both be
Filling in the routes#
We have just defined our list of routes - now it is time to fill them in.
Let's start with our IndexHandler, which is going to be incredibly simple. All it needs to do is serve our index page.
Just to make things easy, we are going to put our
index.html file in the same place as the rest of the files, because there aren't that many of them. But if this were a larger application, we would probably put it in a directory specifically for HTML or potentially templates.
The index page itself will also be rather simple. We only need it for two things, creating a user and logging in, so we can have two forms, one pointing to
LoginPostHandler and the other to
NewUserPostHandler. Sticking with the common web practice of having a user confirm their password when creating an account, the primary difference between the two forms will be a "Confirm Password" field for user creation.