Skip to content

protecting node api

Billy Vlachos edited this page Jul 29, 2019 · 2 revisions

Tutorial - How to protect a node API

In this tutorial we are going to explore how easy it is to protect our API. Conventionally, when we want to protect an API, what we would do is to create a very basic user management system which would allow users to register using a password. We would then define an endpoint where users can login to obtain a bearer token and then use that token for every authentication/authorization request to the API.

This flow is actually fine for small applications, however when the requirements of the API grow, so does the responsibility when it comes to maintaining such a large amount of user records and different flows. Another problem arise when we have multiple APIs as part of a microservice eco system, where implementing such a user flow may be tricky.

Here comes a centralized auth server that keeps track of all the user records and allow for multitenancy, a group of APIs that need protection and possibility for single-sign-in that comes packed with features such as refresh tokens and permissions.

First Steps - Registering our client

In order to be able to protect our api, we need to register it with our auth server. This will allow our server to get to know our api and to know what kinds of protection we need. To do that we will modify our oidc.config.js file in the node-auth-server project. Upon further examination of the config file, we will notice that the array of clients has been abstracted into a different file under the stores folder.

Navigate to src/stores and open the client.store.js file. Inside, create a new object like so:

{
    client_id: '<the id of our API, can be any combination of string/numeric values>',
    logo_uri: '<the url of our API's logo, useful when users are at the API consent screen>(optional)',
    client_name: '<The human readable name of our API> (optional)',

    // The redirect URIs is an array of uris that are registered with our server for the specific API.
    // It can be one or multiple URIs which our server will use to redirect our users. One of the URIs will also
    // be used to send the authorization code, access and id tokens of our users.
    redirect_uris:  [ '<url of our application>/<redirect path>' ],

    // The response toke type our API requires. This depends on if we would like to simply authorize users to 
    // use our API or also authenticate them.
    response_types: ['id_token'],

    // The grant type is simply the flow we will be using for authorizing/authenticating our users.
    // For a list of available types, visit: https://github.com/Official-Codaisseur-Graduate/node-auth-server/wiki/authorization-grants
    grant_types: ['implicit'],
    token_endpoint_auth_method: 'none',
},

Next, define any scopes and claims we need

Depending on our API requirements, we may need to specify some scopes and claims or none at all. If we need our users, for example, to come with additional fields or permissions, we need to define them as claims under a general scope that maps to our application. Typically, scope names are identical to the client_id however you can define any names you wish.

Navigate to src/stores and open the scope.store.js. Inside that, add the scope you need to define along with any claims you want to include in this scope:

exampleScopeName: [ 'claim_one', 'claim_two' etc... ]

Protecting your API

In order to protect our api, we will need to create a simple middleware which will act as the auth middleware for our routes. In this example, we will create a simple Express API.

  1. Create a new folder
  2. cd into the folder and run npm init -y to initialize NPM with the default values
  3. Create the .gitignore file necessary echo node_modules >> .gitignore
  4. Initialize git if required git init
  5. Install the necessary modules for our express API npm i express body-parser util
  6. Create a server.js file and paste the following:
const express = require('express')
const bodyParser = require('body-parser')
const { promisify } = require('util')
const authCheck = require('./auth.middleware');

const app = express()
app.use(bodyParser.json())

// This is an example of an unprotected route
// Note: no middleware applied
app.get('/unprotected', (req, res) => {
    console.log("REACHED UNPROTECTED ENDPOINT, NO AUTH REQUIRED")
    return res.status(200).send("done")
})

// This is another example of a protected route
// NOTE: Middleware also adds the user properties in the request.
app.get('/protected', authCheck, (req, res) => {
    console.log("AUTHENTICATED ROUTE REACHED. USER DETAILS RETRIEVED: ", req.user)
    return res.status(200).send("done")
})

// The default behavior is to throw an error when the token is invalid, 
// so you can add your custom logic to manage unauthorized access as follows:
// NOTE: this needs to be added after assigning the routes
app.use(function (err, req, res, next) {
    if (err.name === 'UnauthorizedError') {
        res.status(401).send('invalid token...');
    }
});

const startServer = async () => {
    const port = process.env.SERVER_PORT || 3000
    await promisify(app.listen).bind(app)(port)
    console.log(`Listening on port ${port}`)
}

startServer()

Note: The second endpoint is now protected with the auth middleware we will be defining in the next step, this middleware checks if the requres includes an Authorization header and that should include the Bearer <token> value with the token aquired from the auth server.

The way you aquire the is beyong the scope of this tutorial as it also depends on the type of grant you want to implement in your API. But for these steps, we will assume that incoming requests bear an authorization header.

  1. Implement the auth middleware. To do that we need two extra modules npm i express-jwt jwks-rsa.
  2. Create a auth.middleware.js file and paste the following:
const jwt = require('express-jwt');
const jwksRsa = require('jwks-rsa');

const checkJwt = jwt({
    secret: jwksRsa.expressJwtSecret({
        cache: false,
        rateLimit: true,
        jwksRequestsPerMinute: 5,
        jwksUri: `https://codaisseur-auth-provider.herokuapp.com/jwks`,
    }),
    audience: '<our_api_id>',
    issuer: 'https://codaisseur-auth-provider.herokuapp.com',
    algorithms: ['RS256'],
});

module.exports = checkJwt;

Let's look into what is happening in the middleware.

The jwks URI is the url where our tokens will be verified against. This url depends on the auth server you'll be protected with. To know more about the endpoint you need to implement in different server, you can refer to your server's .well-known endpoint, typically like so: https://your-auth-server.com/.well-known/openid-configuration.

Next, in the audience, you specify the api_id you defined during the API registration with the server.

The issuer is the actual url of the auth server you'll be using. In our case it's https://codaisseur-auth-provider.herokuapp.com

Deployment

Once you've implemented your API's structure and are ready to deploy, all protected endpoints will require an Authorization header along with the token. Check the next tutorials to learn how to implement these requests from the client.

Clone this wiki locally