A Complete Server: Authentication

Without authentication we can't know who is using our service, and without authorization, we can't control their actions. Our API currently has neither, and this is a problem; we don't want any random user to be able to create and delete our products.

When our API is running on our laptop, we don't need to worry about authentication. We're the only people who can make requests to the API. However, once our API is hosted on a server and is publicly accessible on the internet, we'll want to know who is making the requests. In other words, we'll want our requests to be authenticated. This is important, because without authentication, we won't be able to selectively limit access.

Once we can identify who a request is coming from, we can then make decisions on whether or not we allow access. For example, if we are making the request, we should have full access (it's our API, after all). Conversely, if it's a stranger, they should able to view products, but they should not be authorized to delete anything.

In this chapter we're going to incrementally build up our API's security model. We're going to start with a solid foundation where we can authenticate an admin user, and set up authorization for a set of private endpoints. By the end of the chapter we'll have a full database-backed user model where authenticated users will be authorized to create and view their own orders.

Private Endpoints#

Currently our API has no restrictions on what a user can do. If our API were to be deployed in production, it could be a disaster. Anybody could create an delete products at any time. This is a problem, and so the first thing we need to do is to start restricting access to private endpoints.

The first step towards locking things down is to identify requests coming from an admin user. There are many ways to do this, but we'll start with a very straightforward and common approach: password and cookie.

To start, we come up with a secret password that only the admin user will know. Then we create an endpoint where the admin can log in. The admin user proves their identity by sending their secret password to the log in endpoint. If the password is correct, the API responds with a cookie. When the server receives any subsequent requests with a valid cookie, it knows the user is the admin and can allow access to private endpoints.

Here's a diagram of the admin logging in, getting the cookie, and using it to create a product:

Our admin user authenticating with our API

Using this approach, we can limit access to all of our private endpoints (everything except the ones to view products). Modifying products and orders (create, edit, delete) should only be done by admins. Orders are also private for now; later in this chapter we'll allow customers to create accounts, and we can allow an authenticated customer to create and view their own orders.

Authentication With passport#

To add authentication to our app we'll use the popular module passport. Passport is authentication middleware for Node.js. It's flexible, modular, and works well with express. One of the best things about it is that passport supports many different authentication strategies including username and password, Facebook, Twitter, and more.

The passport module provides the overall framework for authentication, and by using additional strategy modules, we can change how it behaves. We're going to start with username and password authentication, so we'll want to also install the passport-local strategy module.

Once we get everything hooked up, our app will have an endpoint /login that accepts a POST body object with username and password properties. If the username and password combination is correct (we'll get to how we determine this in a second), our app uses the express-session module to create a session for the user. To create a session, our app generates a unique session ID (random bytes like 7H29Jz06P7Uh7lDyuTEMa5TNdZCyDcwM), and it uses that ID as a key to store a value that represents what we know about the user (e.g. {username: 'admin'}). This session ID is then sent to the client as a cookie signed with a secret (if a client alters a cookie the signature won't match and it will be rejected). When the client makes subsequent requests, that cookie is sent back to the server, the server reads the session ID from the cookie (we'll install cookie-parser for this), and the server is able to retrieve the user's session object ({username: 'admin'}). This session object allows our routes to know if each request is authenticated, and if so, who the user is.

By making a few changes to server.js to use passport and passport-local we can start to protect our routes. The first thing we need to do is to require these modules:

We then choose which secret we'll use to sign our session cookies and what our admin password will be:

Simple secrets and passwords are fine for development, but be sure to use more secure ones in production. It's worth reading up on the the dangers of a simplistic session secret and related prevention techniques.

Next, we configure passport to use our passport-local strategy:

We can think of a passport strategy like passport-local as middleware for passport. Similar to how we can use many different middleware functions to operate on our request and response objects as they come into express, we can use many different passport strategies to modify how we authenticate users. Depending on the strategy, configuration will be a little different. Ultimately, the purpose of a strategy is to take some input (e.g. a username and password) and if valid, return a user object.

How `passport` uses strategies

In the case of passport-local, we need to provide a function that accepts username, passport, and a callback. That function checks the provided username and password, and if valid, calls the callback with a user object. If the username and password don't match, the function calls the callback with false instead of the user object.

After passport-local does its thing, passport can handle the rest. passport is able to make sure that the user object is stored in the session and accessible to our middleware functions and route handlers. To do this, we need to provide passport methods for serializing the user object to and deserializing the user object from session storage. Since we're keeping things simple, we'll store the user object in our session as is, and therefore our serialize/deserialize methods will both be identity functions (they just return their arguments/inputs):

We can think of session storage as a simple object with keys and values. The keys are session IDs and the values can be whatever we want. In this case, we want it to be the user object itself. If we kept a lot of user details in the database, we may just want to store the username string when we serialize, and do a database lookup to get the latest, full data when deserializing.

We've now configured passport to use to store user info in the session, but we need to configure express to use cookie-parser and express-session as middleware before it will work properly. Out of the box, express won't automatically parse cookies, nor will it maintain a session store for us.

Note that when configuring expressSession, we provide sessionSecret so we can sign the cookies. We also set resave and saveUninitialized to false as both are recommended by express-session documentation.

Next up, we tell express to use passport as middleware by using its initialize() and session() methods:

We've now got passport all hooked up, and our app can take advantage of it. The two things we need to do are to handle logins and to protect routes. Here's how we have passport handle the login:

 

This page is a preview of Fullstack Node.js

Start a new discussion. All notification go to the author.