api

Express.js API

Express.js API

In this tutorial, you are going to learn how to integrate a JavaScript Express.js resource server with FusionAuth. You will protect an API resource from unauthorized usage. You’ll be building it for ChangeBank, a global leader in converting dollars into coins.

The Docker Compose file and source code for a complete application are available at https://github.com/FusionAuth/fusionauth-quickstart-javascript-express-api.

Prerequisites

For this quickstart, you’ll need:

General Architecture

A client wants access to an API resource at /resource. However, it is denied this resource until it acquires an access token from FusionAuth.

ClientResource ServerFusionAuthGET /resource404 Not AuthorizedPOST /api/login200 Ok(token)GET /resource200 Ok(resource)ClientResource ServerFusionAuth

Resource Server Authentication with FusionAuth

While the access token is acquired via the Login API above, this is for simplicity of illustration. The token can be, and typically is, acquired through one of the OAuth grants.

Getting Started

In this section, you’ll get FusionAuth up and running and create a resource server that will serve the API.

Clone The Code

First off, grab the code from the repository and change into that directory.

git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-express-api.git
cd fusionauth-quickstart-javascript-express-api

All shell commands in this guide can be entered in a terminal from the fusionauth-quickstart-javascript-express-api folder location. On Windows, you need to replace forward slashes with backslashes in paths.

All the files you’ll create in this guide already exist in the complete-application subfolder, if you prefer to copy them.

Run FusionAuth Via Docker

You’ll find a Docker Compose file docker-compose.yml and an environment variables configuration file .env in the root folder of the repository.

Assuming you have Docker installed, you can start FusionAuth on your machine with the following.

docker compose up -d

This will start three containers, one each for FusionAuth, Postgres, and Elasticsearch.

Here you are using a bootstrapping feature of FusionAuth, called Kickstart. When FusionAuth starts for the first time, it will look at the kickstart/kickstart.json file and configure FusionAuth to your specified state.

If you ever want to reset the FusionAuth system, delete the volumes created by docker compose by executing docker compose down -v, then rerun docker compose up -d.

FusionAuth will be initially configured with these settings:

You can log in to the FusionAuth admin UI and look around if you want, but with Docker/Kickstart everything will already be configured correctly.

The .env and kickstart.json files contain passwords. In a real application, always add these files to your .gitignore file and never commit secrets to version control.

Create A Basic Express.js Application

Now, you are going to create an Express.js API application. While this section builds a simple API, you can use the same configuration to integrate an existing API with FusionAuth.

You are going to be building an API backend for a banking application called ChangeBank. This API will have two endpoints.

Both endpoints will be protected such that a valid JSON web token (JWT) will be required in a cookie or Authorization header in order for the endpoints to be accessed. Additionally, the JWT must have a roles claim containing the appropriate role to use the endpoint.

Create the starter app called your-application for this API. Accept all the defaults.

npx express-generator@latest your-application --no-view
cd your-application
npm install
npm audit fix --force
npm install dotenv
npm install jose

Here you set up your application and add project dependencies. The dotenv library allows the code to read your .env environment variable file. Additionally, the jose library allows you to validate the authenticity of the JWT.

Now, create the .env file in your your-application folder and add the following to it.

This is a different environment file to the one in the root folder used by Docker for FusionAuth

CLIENT_ID="e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
CLIENT_SECRET="super-secret-secret-that-should-be-regenerated-for-production"
BASE_URL="http://localhost:9011"
AUTH_URL="http://localhost:9011/oauth2"
AUTH_CALLBACK_URL="http://localhost:3000/auth/callback"

Authentication

Before you configure authentication, you need to change the default app.js file created by Express.js to add an authentication check to every call made to the API.

Add the following lines in app.js just above app.use('/', indexRouter);.

const verifyJWT = require('./services/verifyJWT');
app.use(verifyJWT);

The line, const verifyJWT = require('./services/verifyJWT');, imports middleware to add authentication to the app. Let’s create that file now.

First create the services directory under your-application.

mkdir services

In this services folder, add the file verifyJWT.js, and copy in the following code.

require('dotenv/config');
const jose = require('jose');

const jwksClient = jose.createRemoteJWKSet(
  new URL(`${process.env.BASE_URL}/.well-known/jwks.json`)
);

const verifyJWT = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  const tokenFromHeader = authHeader ? authHeader.split(' ')[1] : null;
  const access_token = req.cookies['app.at'] || tokenFromHeader;
  if (!access_token) {
    res.status(401);
    res.send({ error: 'Missing token cookie and Authorization header' });
  } else {
    try {
      const result = await jose.jwtVerify(access_token, jwksClient, {
        issuer: process.env.BASE_URL,
        audience: process.env.CLIENT_ID,
      });
      req.verifiedToken = access_token;
      next();
    } catch (e) {
      if (e instanceof jose.errors.JOSEError) {
        res.status(401);
        res.send({ error: e.message, code: e.code });
      } else {
        console.dir(`Internal server error: ${e}`);
        res.status(500);
        res.send({ error: JSON.stringify(e) });
      }
    }
  }
};

module.exports = verifyJWT;

The first line, require('dotenv/config');, tells the application to use the settings from .env you created earlier.

The function verifyJWT gets the JWT access token from a cookie or the Authorization header in the request, and it uses jose.js to check the signature is correct for your installation of FusionAuth. If either the token is missing or it is not signed correctly, an error is returned. Otherwise, the next function in the Express.js middleware stack is called.

Now, you’ll add one more file that will check if the user has a role with permissions necessary to call an API endpoint.

In the services folder, add the file hasRole.js and copy in the following code.

const jose = require('jose');

function hasRole(roles) {
  return (req, res, next) => {
    const decodedToken = jose.decodeJwt(req.verifiedToken);
    if (roles.some((role) => decodedToken.roles.includes(role))) return next();
    res.status(403);
    res.send({ error: `You do not have a role with permissions to do this.` });
  }
}

module.exports = hasRole;

This function is given a list of roles, any of which allow the user’s request. It returns a middleware function that checks if the JWT contains one of the required roles, and either returns an error or allows the next function in the middleware stack to proceed.

You do not need to check for the existence of a token in a cookie here because the previous middleware function you wrote already did that.

Create The Routes

All that’s left to do is write the two endpoints that will demonstrate your authentication.

Overwrite routes/index.js with the following code.

var express = require('express');
var router = express.Router();
const hasRole = require('../services/hasRole');

router.get('/', function (req, res, next) {
  res.render('index', { title: 'Express' });
});

router.post('/panic', hasRole(['teller']), function (req, res, next) {
  res.json({ message: "We've called the police!" });
});

router.get('/make-change', hasRole(['customer', 'teller']), function (req, res, next) {
  const amount = req.query.total;
  const result = { total: 0, nickels: 0, pennies: 0};
  result.total = Math.trunc(parseFloat(amount)*100)/100;
  result.nickels = Math.floor(amount / 0.05);
  const pennies = ((amount - (0.05 * result.nickels)) / 0.01);
  result.pennies = Math.ceil((Math.trunc(pennies*100)/100));
  const error = ! /^(\d+(\.\d*)?|\.\d+)$/.test(amount);
  if (error)
    return res.status(400).json({ error: 'Invalid or missing "total" parameter' })
  res.json(result);
});

module.exports = router;

The POST panic route uses the hasRole function with the 'teller' parameter to disallow non-tellers.

The GET make-change route uses hasRole function with the ['customer', 'teller'] array as a parameter to allow either customers or tellers to make change. This route checks that the user has supplied a number for total, then either returns change as JSON or an error if the total was invalid.

Run The Application

Start the API server by running the command below in the your-application directory:

npm run start

Get A Token

There are several ways to acquire a token in FusionAuth, but for this example, you will use the Login API to keep things simple.

First, let’s try the requests as the teller@example.com user. Based on the configuration, this user has the teller role and should be able to use both /make-change and /panic.

Acquire an access token for teller@example.com by making the following request in a new terminal window.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "teller@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Copy the token from the response, which should look like this:

{
  "token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6InVOYl9iQzFySHZZTnZMc285VzRkOEprZkxLWSJ9.eyJhdWQiOiJlOWZkYjk4NS05MTczLTRlMDEtOWQ3My1hYzJkNjBkMWRjOGUiLCJleHAiOjE2ODkzNTMwNTksImlhdCI6MTY4OTM1Mjk5OSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDExIiwic3ViIjoiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMTExMTExMTExMTExIiwianRpIjoiY2MzNWNiYjUtYzQzYy00OTRjLThmZjMtOGE4YWI1NTI0M2FjIiwiYXV0aGVudGljYXRpb25UeXBlIjoiUEFTU1dPUkQiLCJlbWFpbCI6InRlbGxlckBleGFtcGxlLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJhcHBsaWNhdGlvbklkIjoiZTlmZGI5ODUtOTE3My00ZTAxLTlkNzMtYWMyZDYwZDFkYzhlIiwicm9sZXMiOlsiY3VzdG9tZXIiLCJ0ZWxsZXIiXSwiYXV0aF90aW1lIjoxNjg5MzUyOTk5LCJ0aWQiOiJkN2QwOTUxMy1hM2Y1LTQwMWMtOTY4NS0zNGFiNmM1NTI0NTMifQ.WLzI9hSsCDn3ZoHKA9gaifkd6ASjT03JUmROGFZaezz9xfVbO3quJXEpUpI3poLozYxVcj2hrxKpNT9b7Sp16CUahev5tM0-4_FaYlmUEoMZBKo2JRSH8hg-qVDvnpeu8nL6FXxJII0IK4FNVwrQVFmAz99ZCf7m5xruQSziXPrfDYSU-3OZJ3SRuvD8bMopSiyRvZLx6YjWfBsvGSmMXeh_8vHG5fVkq5w1IkaDdugHnivtJIivHuCfl38kQBgw9rAqJLJoKRHHW0Ha7vHIcS6OCWWMDIIVspLyQNcLC16pL9Nss_5v9HMofow1OvQ9sUSMrbbkipjKq2peSjG7qA",
  "tokenExpirationInstant": 1689353059670,
  "user": {
      ...
  }
}

Make The Request

The code is set up to extract the token from either a cookie or the Authorization header so depending on your preference you can replace --cookie 'app.at=<your_token>' with --header 'Authorization: Bearer <your_token>' when making requests to the API.

If you use a cookie, make sure you store it in a secure, HTTPOnly cookie to avoid exfiltration attacks. See Storing OAuth Tokens for more information.

Make a request to /make-change with a query parameter total=1.02. Use the token as the app.at cookie.

curl --cookie 'app.at=<your_token>' 'http://localhost:3000/make-change?total=1.02'

Alternatively you can make the same request by passing your token in the Authorization header.

curl --location 'http://localhost:3000/make-change?total=1.02' \
--header 'Authorization: Bearer <your_token>'

Your response should look like this:

{
  "total":1.02,
  "nickels":20,
  "pennies":2
}

You were successfully authorized. You can try making the request without the JWT or with a different string rather than a valid token, and see that you are denied access.

Next, call the /panic endpoint because you are in trouble!

curl --location --request POST 'http://localhost:3000/panic' --cookie 'app.at=<your_token>'

This is a POST not a GET because you want all your emergency calls to be non-idempotent.

Your response should look like this:

{
  "message": "We've called the police!"
}

Nice, help is on the way!

Now, let’s try as customer@example.com, which has the role customer. Acquire a token for customer@example.com.

curl --location 'http://localhost:9011/api/login' \
--header 'Authorization: this_really_should_be_a_long_random_alphanumeric_value_but_this_still_works' \
--header 'Content-Type: application/json' \
--data-raw '{
  "loginId": "customer@example.com",
  "password": "password",
  "applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"
}'

Now, use the token in that response to call /make-change with a query parameter total=3.24.

curl --cookie 'app.at=<your_token>' 'http://localhost:3000/make-change?total=3.24'

Your response should look like this:

{
  "total": 3.24,
  "nickels": 64,
  "pennies": 4
}

So far so good. Now, let’s try to call the /panic endpoint. (We’re adding the -i flag to see the headers of the response.)

curl -i --request POST 'http://localhost:3000/panic' --cookie 'app.at=<your_token>'

Your response should look like:

HTTP/1.1 403 Forbidden
X-Powered-By: Express
Content-Type: application/json; charset=utf-8
Content-Length: 63
ETag: W/"3f-eBPCxvxzYQmL0BG1KPuKqm5VcP4"
Date: Tue, 12 Sep 2023 09:46:52 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"error":"You do not have a role with permissions to do this."}

Uh oh, I guess you are not allowed to do that.

Enjoy your secured resource server!

Next Steps

This quickstart is a great way to get a proof of concept up and running quickly, but to run your API in production, there are some things you’re going to want to do.

FusionAuth Integration

Security

Troubleshooting

Ensure FusionAuth is running in the Docker container. You should be able to log in as the admin user, admin@example.com with a password of password at http://localhost:9011/admin.

Make sure you are making a POST call and using a token with the teller role.

Ensure you have the latest version of Docker, or try docker-compose instead. If on Linux, install docker.ce instead of docker.io.

You can always pull down a complete running application and compare what’s different.

git clone https://github.com/FusionAuth/fusionauth-quickstart-javascript-express-api.git
cd fusionauth-quickstart-javascript-express-api
docker compose up -d
cd complete-application
npm install
npm run start