Lesson 28. Adding API Security

In this lesson, you apply a few security strategies to your API routes. Without a browser to store cookies, some external applications may find it difficult to use your API without a way to verify the user’s identity. First, you implement some basic security by providing an API token that must be appended to each request. Then you improve that strategy by generating a unique API key for each user upon account creation. Last, you explore JSON Web Tokens (JWT), a system of hashing user data and exchanging tokens to authenticate user accounts without a browser.

This lesson covers

  • Adding security-token-verification middleware
  • Creating a pre("save") hook to generate API keys
  • Implementing JWT header authentication
Consider this

You built a robust API for the recipe application. Your endpoints include routes to create new users and update existing users. Because an API endpoint can be accessed from any device that can make an HTTP request, there’s no telling who might make a request to your API without first creating an account and storing session data on the server.

Having some form of security on your API routes ensures that your data doesn’t fall into the wrong hands.

28.1. Implementing simple security

Unit 5 guided you through user-account creation and authentication. With the help of a few packages, you created a thorough process of validating and encrypting user data and of ensuring that those users were authenticated before getting access to certain pages.

Even without the help of external packages, you can take some simple steps to protect your API. The first method you’ll use in this lesson is generating an API token that must be used by users accessing your API. Users need to have a token because they may not be using a browser to access the API, so your current implementation with Passport.js, cookies, and sessions may not work with the client. An additional token reduces this risk, ensuring that only users who make requests with a valid token can see data. You could add app.set("token", process.env.TOKEN || "recipeT0k3n") to main.js, for example. Then this application variable would be set to whatever you use as the TOKEN environment variable or default to recipeT0k3n. The token could be retrieved by using app.get("token").

Because you want to monitor incoming requests to the API in the apiRoutes module, set the token as a constant in usersController.js in the api folder, using const token = process.env.TOKEN || "recipeT0k3n". This token will be used by middleware within usersController.js to verify incoming API requests. Create that middleware function by adding the code in listing 28.1 to usersController.js.

This middleware function, verifyToken, checks for a query param called apiToken that matches the token you set earlier. If the tokens match, call next to continue the middleware chain; otherwise, pass an error with a custom message. This error reaches your error-handling middleware and displays the message as JSON.

Listing 28.1. Adding middleware function to verify API token in usersController.js
verifyToken: (req, res, next) => {                1
  if (req.query.apiToken === token) next();       2
  else next(new Error("Invalid API token."));     3
}

  • 1 Create the verifyToken middleware function with the next parameter.
  • 2 Call the next middleware function if tokens match.
  • 3 Respond with error message if tokens don’t match.

To add the usersController.verifyToken middleware so that it runs before every API request is handled, you can add router.use(usersController.verifyToken), as the first function in apiRoutes.js. You also need to require the users controller by adding const usersController = require("../controllers/usersController") to apiRoutes.js.

Restart your application, and when you visit http://localhost:3000/api/courses, notice the following error message: {"status":500, "message":"Invalid API token."}. This message is a good sign. It means that your API validation is working because you didn’t make a request by using a valid API token.

To bypass this message, add the apiToken query parameter. Visiting http://localhost: 3000/api/courses?apiToken=recipeT0k3n should result in a display of the original course data in JSON format. If you choose to implement your API security this way, you need to share this token with your trusted users. To get your AJAX requests to work, add the ?apiToken=recipeT0k3n query parameter to those URLs as well in recipeApp.js.

This simple security barrier is definitely a start, but you can imagine that it quickly becomes an unreliable system as more users require the token to access your API. The more users who have access to the same token, the more likely it is for that token to fall into the hands of nonusers. When you’re quickly building an application that requires a thin layer of security, this approach may be sufficient. When the application is live online, however, you’ll want to modify the API security to treat each user request uniquely.

In the next section, you explore ways to keep the token unique for each user.

Quick check 28.1

Q1:

Why might you store a secret token in process.env.TOKEN?

QC 28.1 answer

1:

You can store sensitive or secret data in process.env as environmental variables. These variables are normally stored on the server but don’t need to appear in the code. This practice makes it easier to change the token directly on the server (you don’t have to change the code each time), and it’s a more-secure convention.

 

28.2. Adding API tokens

You just constructed a middleware function to verify API tokens passed as query parameters in the URL. This method is effective at securing your API, but it doesn’t prevent nonusers from getting their hands on the one and only token.

To improve this system, add a custom token to each user account. Do this by adding a new apiToken field to the user schema that’s of type String. Next, build a pre("save") hook on the User model to generate an API token that’s unique to that user upon account creation. Before you get to the code, use a Node.js package to help with the token generation.

The rand-token package provides some simple tools for creating new alphanumeric tokens of your desired length. Run npm install rand-token -S to install the rand-token package in this project, and require it in user.js by adding const randToken = require ("rand-token").

Add the code in the next listing to user.js. This code first checks whether the user’s -apiToken field is set. If it isn’t, generate a new unique 16-character token with rand-Token.generate.

Listing 28.2. Creating a pre(save) hook to generate an API token in user.js
userSchema.pre("save", function(next) {
  let user = this;
  if (!user.apiToken) user.apiToken =
 randToken.generate(16);                 1
  next();
});

  • 1 Check for an existing API token and generate a new one with randToken.generate.
Note

You can improve the functionality here by comparing the generated token with other users’ tokens to ensure that no duplicity occurs.

Next, add the apiToken field as an item in the table on the user’s show page. This way, when a new user visits their profile page, they’ll have access to their API token. In figure 28.1, for example, my user account has the token 2plMh5yZMFULOzpx.

Figure 28.1. Displaying the API token on the user’s show page

To use this token, you need to modify the verifyToken middleware to check the apiToken query param against the tokens in your database. Change verifyToken in /api/users-Controller.js to use the code in listing 28.3.

In this modified middleware function, you grab the token as the query parameter. If a token appears in the URL, search the user database for a single user who has that API token. If such a user exists, continue to the next middleware function. If no user with that token exists, if an error occurs in the query, or if no query parameter was used, pass an error.

Listing 28.3. Improving the token verification action in usersController.js
verifyToken: (req, res, next) => {
  let token = req.query.apiToken;
  if (token) {                                         1
    User.findOne({ apiToken: token })                  2
      .then(user => {
        if (user) next();                              3
        else next(new Error("Invalid API token."));
      })
      .catch(error => {                                4
        next(new Error(error.message));
      });
  } else {
    next(new Error("Invalid API token."));
  }
}

  • 1 Check whether a token exists as the query parameter.
  • 2 Search for a user with the provided API token.
  • 3 Call next if a user with the API token exists.
  • 4 Pass an error to error handler.

Restart your application, and create a new user account. Visit that new user’s show page, and locate the apiToken value. Then visit http://localhost:3000/api/courses? api-Token= followed by the API token for that user. The [email protected] user, for example, would use the following URL: http://localhost:3000/api/courses?apiToken= 2plMh5yZMFULOzpx. You should see the list of courses in JSON as before.

This new system reduces the vulnerability of having a single API token for all users. With the API token connected to a user account, you could also verify the user’s information in your database and keep metrics on the number or quality of that user’s API requests. To get your client-side JavaScript to use this token in your API calls, you can add a hidden element to layout.ejs with the current user’s token. You could add <div id="apiToken" data-token="<%= currentUser.apiToken %>" style="display: none;"> within the block to check whether a user is logged in, for example. Then, when the document is ready in recipeApp.js, you can locate the token, use it with let apiToken = $("#apiToken").data ("token"), and call your Ajax request on /api/courses?apiToken=${apiToken}.

Still, you can take a more-secure approach to building API authentication in which a web browser isn’t necessarily involved. That method uses JSON web tokens (JWT).

Quick check 28.2

Q1:

What does randToken.generate(16) do?

QC 28.2 answer

1:

This method generates a random 16-character alphanumeric token.

 

28.3. Using JSON web tokens

You can build a secure API by using cookies, but the API’s functionality still depends on its clients to support and store those cookies. Consider someone who writes a script to run requests against your API solely from their terminal window, for example. In this case, if you want to apply user authentication on incoming requests, you need some way to keep track of which users are requesting and whether they’ve recently logged in. Without a visual login page, that task can be difficult. You can try some alternative solutions, one of which is using JSON web tokens.

JSON web tokens (JWT) are signed or encrypted data passed between the server and client as a means of representing an authenticated user request. Ultimately, JWTs are like sessions in a different format and used differently in web communication. You can think of JWTs as being like API tokens that are regenerated on every login. JWTs contain three parts, as defined in table 28.1.

Table 28.1. Parts of JWTs

JWT part

Description

Header A JSON object detailing how the data in the JWT is prepared and hashed.
Payload The data stored in the JWT, used to verify the user who previously authenticated. The payload normally includes the user’s ID.
Signature A hashed code using the header and payload values.
Tip

The smaller the payload, the smaller the JWT and the faster it’s sent with each response.

These three values together offer a unique arrangement of data indicating the recent login status for a specific user. First, the user makes a request and passes their email and password. The server responds with an encoded JWT verifying the user’s correct login information. For each subsequent user request, that same JWT must be sent back to the server. Then the server verifies the JWT by decoding its values and locating the user specified in the payload. Unlike in password encryption with Passport.js and bcrypt, JWTs aren’t encrypted through hashing and salting. JWTs are encoded, which means that the server can decode the JWT to reveal its contents without needing to know some secret value set by the user.

In this section, you apply JWT API security with the help of the jsonwebtoken package. Install the jsonwebtoken package by running npm i jsonwebtoken -S in terminal. Because you’re going to use JWTs for user verification in the API, require jsonwebtoken in users-Controller.js with const jsonWebToken = require("jsonwebtoken").

To use JWTs, you need to allow the user to log in without a browser. Create a new API login action by adding the code in listing 28.4 to usersController.js.

Note

You can find more information on the jsonwebtoken package at https://github.com/auth0/node-jsonwebtoken.

This action uses the Passport.js local strategy that you set up in lesson 24. Through the authenticate method, verify that the user email address and password match that of a user in the database. Then, through a callback function, if a user is found with the matching email and password, use jsonWebToken.sign to create a token with the user’s ID and an expiration date set to one day from the time of signing. Finally, respond with a JSON object with a success tag and the signed token; otherwise, respond with the error message.

Listing 28.4. Creating a login action for the API in usersController.js
apiAuthenticate: (req, res, next) => {                          1
  passport.authenticate("local",(errors, user) => {
    if (user) {
      let signedToken = jsonWebToken.sign(                      2
        {
          data: user._id,
          exp: new Date().setDate(new Date().getDate() + 1)
        },
        "secret_encoding_passphrase"
      );
      res.json({
        success: true,
        token: signedToken                                      3
      });
    } else
      res.json({
        success: false,
        message: "Could not authenticate user."                 4
      });
  })(req, res, next);
}

  • 1 Authenticate with the passport.authenticate method.
  • 2 Sign the JWT if a user exists with matching email and password.
  • 3 Respond with the JWT.
  • 4 Respond with an error message.

Now this token can be used for 24 hours to make requests to secured API endpoints.

Next, add the following POST route to apiRoutes.js: router.post(/login”, usersController.apiAuthenticate). You can generate the token without a browser by making a POST request to the /api/login route with your email and password in the body. To do so, run a curl command in terminal, such as curl -d "[email protected]&password=12345" http://localhost:3000/api/login. In this example, the -d flag indicates that the user is posting their email and password as data to the provided URL. After running this command, you should expect a response similar to the response in the next listing.

Listing 28.5. Example response for a successful JWT authentication in terminal
{"success":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
 .eyJkYXRhIjoiNTljOWNkN2VmNjU5YjMwMjk4YzkzMjY4IiwiZXhwIjox
 NTA2NDk2NDMyODc5LCJpYXQiOjE1MDY0MTAwMzJ9.Gr7gPyodobTAXh1p
 VuycIDxMEf9LyPsbrR4baorAbw0"}                                1

  • 1 Display of a successful response with a JWT after authentication.

To secure all the API endpoints, add an action to verify incoming JWTs and add that middleware for every API route. Add the code in listing 28.6 to usersController.js.

First, pull the incoming token from the request header. Then, if a token exists, use jsonWebToken.verify along with the token and secret passphrase to decode the token and verify its authenticity. The following callback provides any errors that may have occurred, as well as the decoded payload. You can check whether the payload has a value. If so, pull the user’s ID from payload.data, and query the database for a user with that ID. If no such user exists, that user’s account may have been deleted, or the JWT may have been tampered with, so return an error message. If the user ID matches, call next and move on to the API endpoint. This method of communication continues until the token expires and the user creates a new JWT.

Listing 28.6. Creating a verification action for the API in usersController.js
verifyJWT: (req, res, next) => {
  let token = req.headers.token;                             1
  if (token) {
    jsonWebToken.verify(                                     2
      token,
      "secret_encoding_passphrase",
      (errors, payload) => {
        if (payload) {
          User.findById(payload.data).then(user => {         3
            if (user) {
              next();                                        4
            } else {
              res.status(httpStatus.FORBIDDEN).json({
                error: true,
                message: "No User account found."
              });
            }
          });
        } else {
          res.status(httpStatus.UNAUTHORIZED).json({
            error: true,
            message: "Cannot verify API token."              5
          });
          next();
        }
      }
    );
  } else {
    res.status(httpStatus.UNAUTHORIZED).json({
      error: true,
      message: "Provide Token"                               6
    });
  }
}

  • 1 Retrieve the JWT from request headers.
  • 2 Verify the JWT, and decode its payload.
  • 3 Check for a user with the decoded user ID from the JWT payload.
  • 4 Call the next middleware function if a user is found with the JWT ID.
  • 5 Respond with an error message if the token can’t be verified.
  • 6 Respond with an error message if no token is found in the request headers.

The final step is placing this verifyJWT middleware function before any API request is processed. Add router.use(usersController.verifyJWT) to apiRoute.js below the login route and above all other routes. This step ensures that every route needs to use the verifyJWT middleware except for the login route, which is used to generate your JWT.

Note

At this point, you no longer need your token generator hook on the User model or any remnants of the past two API security techniques to use JWTs. You may want to keep these recently implemented API security techniques in place, however, as a fallback to access your API. More work is needed to get these security approaches to work together.

You can test your JWT by running another curl command in terminal and identifying the token in the request headers. With the token from listing 28.5, that command looks like listing 28.7.

In this command, you use the -H flag to indicate a header key-value pair for your JWT in quotation marks. By making a request and passing a valid JWT, you should gain access to the application’s data.

Note

You need to remove the usersController.verifyToken action to make this new approach work. Otherwise, your application will look for both a JWT header and an apiToken.

Listing 28.7. Creating a verification action for the API in usersController.js
curl  -H "token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkY
 XRhIjoiNTljOWNkN2VmNjU5YjMwMjk4YzkzMjY4IiwiZXhwIjoxNT
 A2NDk2NDMyODc5LCJpYXQiOjE1MDY0MTAwMzJ9.Gr7gPyodobTAX
 h1pVuycIDxMEf9LyPsbrR4baorAbw0" http://localhost:3000
 /api/courses                                             1

  • 1 Make a request with JWT in the headers.
Warning

The way you’re building your API to use JWTs will interfere with the work you’ve already done in your client-side Ajax request. Consider this section to be an introduction to using JWTs, not necessarily a replacement for the security you’ve implemented in the recipe application so far.

If your request is successful, you should expect to see the same list of courses as the JSON from the first section of this lesson. If you plan to use JWTs for securing your API, you need to specify to the users of your API exactly how you expect them to authenticate and verify their tokens. One way is to create a view with an additional login form where a user can post their email and password to get an API token in response. That token can be stored temporarily on the User model like the random token in the preceding section.

Note

Using JWTs requires the client to store the token in some way. Not being able to store the JWT temporarily makes it impossible to create future requests after the token is created on login.

JWTs can help prevent attacks on your application’s data and secure access through your API, but this requires more steps to implement. Ultimately, you may find that it makes more sense to start with a simpler approach, such as generating random tokens for each user.

Quick check 28.3

Q1:

Why do you pass the JWT in the header of the request?

QC 28.3 answer

1:

You could pass the JWT in the body of the request, but because not all requests will be POST, the headers offer a more convenient place.

 

Summary

In this lesson, you learned how to implement three security tokens on your API. The first strategy is a simple security token that can be used by all clients. The second strategy requires generating a new random token for each user upon creation. In the third approach, you use JWTs to provide the most-secure option for authenticating users to access your API. In lesson 29 (this unit’s capstone exercise), you have an opportunity to build an API with some of the functionality introduced in this unit.

Try this

Now that you have some basic security options to choose among, try creating more API routes that require JWTs. You can also exclude certain routes from requiring a token, such as the login route. Pick two routes to exclude from your API security.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.16.54.63