Integrate Your Express API With FusionAuth
In this article, you are going to learn how to integrate an Express API with FusionAuth. This presupposes you've built an application that is going to retrieve an access token from FusionAuth via one of the OAuth grants. The grant will typically be the Authorization Code grant for users or the Client Credentials grant for programmatic access.
The token provided by FusionAuth can be stored by the client in a number of locations. For server side applications, it can be stored in a database or on the file system. In mobile applications, store them securely as files accessible only to your app. For a browser application like a SPA, use a cookie if possible and server-side sessions if not.
Here’s a typical API request flow before integrating FusionAuth with your Express API.
Here’s the same API request flow when FusionAuth is introduced.
This document will walk through the use case where a Express API validates the token. You can also use an API gateway to verify claims and signatures. For more information on doing that with FusionAuth, visit the API gateway documentation.
Prerequisites
For this tutorial, you’ll need to have Node.js installed.
You'll also need Docker, since that is how you’ll install FusionAuth.
The commands below are for macOS, but are limited to mkdir
and cd
, which have equivalents in Windows and Linux.
Download and Install FusionAuth
First, make a project directory:
mkdir integrate-fusionauth && cd integrate-fusionauth
Then, install FusionAuth:
curl -o docker-compose.yml https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/FusionAuth/fusionauth-containers/master/docker/fusionauth/.env
docker compose up -d
Create a User and an API Key
Next, log into your FusionAuth instance. You’ll need to set up a user and a password, as well as accept the terms and conditions.
Then, you’re at the FusionAuth admin UI. This lets you configure FusionAuth manually. But for this tutorial, you're going to create an API key and then you’ll configure FusionAuth using our client library.
Navigate to + button to add a new API Key. Copy the value of the Key field and then save the key.
It might be a value like CY1EUq2oAQrCgE7azl3A2xwG-OEwGPqLryDRBCoz-13IqyFYMn1_Udjt
.
Doing so creates an API key that can be used for any FusionAuth API call. Save that key value off as you’ll be using it later.
Configure FusionAuth
Next, you need to set up FusionAuth. This can be done in different ways, but we are going to use the TypeScript client library. The instructions below use npm
on the command line, but you can use the client library with an IDE of your preference as well.
First, make a directory:
mkdir setup-fusionauth && cd setup-fusionauth
Now, copy and paste the following code into package.json
.
{
"name": "fusionauth-example-typescript-client",
"version": "1.0.0",
"description": "Example of setting up an application with typescript.",
"scripts": {
"setup-angular": "node setup-angular.js",
"setup-express": "node setup-express.js",
"setup-flutter": "node setup-flutter.js",
"setup-react": "node setup-react.js"
},
"author": "Dan Moore",
"license": "Apache-2.0",
"dependencies": {
"@fusionauth/typescript-client": "^1.45.0"
}
}
Now you need to install the dependencies in package.json
.
npm install
Then copy and paste the following code into setup.js
. This file uses the FusionAuth API to configure an Application and more to allow for easy integration.
const {FusionAuthClient} = require('@fusionauth/typescript-client');
APPLICATION_ID = "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e";
RSA_KEY_ID = "356a6624-b33c-471a-b707-48bbfcfbc593"
// You must supply your API key as an environment variable
const fusionAuthAPIKey = process.env.fusionauth_api_key;
if (! fusionAuthAPIKey ) {
console.log("please set api key in the fusionauth_api_key environment variable")
process.exit(1)
}
async function getTenant(client) {
tenant = null
try {
clientResponse = await client.retrieveTenants()
tenant = clientResponse.response.tenants[0]
} catch (error) {
console.log("couldn't find tenants " + JSON.stringify(error))
process.exit(1)
}
return tenant
}
async function patchTenant(client, tenant) {
try {
clientResponse = await client.patchTenant(tenant["id"], {"tenant": {"issuer":"http://localhost:9011"}})
} catch (error) {
console.log("couldn't update tenant " + JSON.stringify(error))
process.exit(1)
}
}
async function generateKey(client) {
try {
clientResponse = await client.generateKey(RSA_KEY_ID, {"key": {"algorithm":"RS256", "name":"For JSExampleApp", "length": 2048}})
} catch (error) {
console.log("couldn't create RSA key " + JSON.stringify(error))
process.exit(1)
}
}
async function createApplication(client) {
application = {}
application["name"] = "JSExampleApp"
application["oauthConfiguration"] = {}
application["oauthConfiguration"]["authorizedRedirectURLs"] = ["http://localhost:3000"]
application["oauthConfiguration"]["requireRegistration"] = true
application["oauthConfiguration"]["enabledGrants"] = ["authorization_code", "refresh_token"]
application["oauthConfiguration"]["logoutURL"] = "http://localhost:3000/logout"
application["oauthConfiguration"]["clientSecret"] = "change-this-in-production-to-be-a-real-secret"
// assign key from above to sign our tokens. This needs to be asymmetric
application["jwtConfiguration"] = {}
application["jwtConfiguration"]["enabled"] = true
application["jwtConfiguration"]["accessTokenKeyId"] = RSA_KEY_ID
application["jwtConfiguration"]["idTokenKeyId"] = RSA_KEY_ID
try {
clientResponse = await client.createApplication(APPLICATION_ID, {"application": application})
} catch (error) {
console.log("couldn't create application " + JSON.stringify(error))
process.exit(1)
}
}
async function getUser(client) {
user = null
try {
// should only be one user
clientResponse = await client.searchUsersByQuery({"search": {"queryString":"*"}})
user = clientResponse.response.users[0]
} catch (error) {
console.log("couldn't find user " + JSON.stringify(error))
process.exit(1)
}
return user
}
// patch the user to make sure they have a full name, otherwise OIDC has issues
// TODO test check for errorResponse
async function patchUser(client, user) {
try {
clientResponse = await client.patchUser(user["id"], {"user": {"fullName": user["firstName"]+" "+user["lastName"]}})
} catch (error) {
console.log("couldn't patch user " + JSON.stringify(error))
process.exit(1)
}
}
async function registerUser(client, user) {
try {
clientResponse = await client.register(user["id"], {"registration":{"applicationId":APPLICATION_ID}})
} catch (error) {
console.log("couldn't register user " + JSON.stringify(error))
process.exit(1)
}
}
async function main(client) {
tenant = await getTenant(client)
await patchTenant(client, tenant)
await generateKey(client)
await createApplication(client)
user = await getUser(client)
await patchUser(client, user)
await registerUser(client, user)
console.log(user)
}
const client = new FusionAuthClient(fusionAuthAPIKey, 'http://localhost:9011');
main(client)
Then, you can run the setup script.
The setup script is designed to run on a newly installed FusionAuth instance with only one user and no tenants other than Default
. To follow this guide on a FusionAuth instance that does not meet these criteria, you may need to modify the script above.
Refer to the Typescript client library documentation for more information.
fusionauth_api_key=YOUR_API_KEY_FROM_ABOVE node setup.js
If you are using PowerShell, you will need to set the environment variable in a separate command before executing the script.
$env:fusionauth_api_key='YOUR_API_KEY_FROM_ABOVE'
node setup.js
If you want, you can log into your instance and examine the new Application the script created for you.
Create Your Express API
Now you are going to create an Express API. While this section uses a simple Express API, you can use the same steps to integrate any Express API with FusionAuth.
First, make a directory.
mkdir ../setup-express-api && cd ../setup-express-api
Create a package.json
file with the following contents to set up the dependencies.
{
"name": "fusionauth-example-express-api",
"version": "1.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.6",
"cors": "~2.8.5",
"debug": "~4.3.4",
"dotenv": "~16.3.1",
"express": "~4.18.2",
"http-errors": "~2.0.0",
"jose": "~4.14.4",
"morgan": "~1.10.0"
}
}
Now, install the needed packages.
npm install
You are going to create some files in different directories, so pay attention to the final directory structure that you should have after completing these steps.
├── app.js
├── bin
│ └── www
├── middlewares
│ └── authentication.js
├── package.json
├── package-lock.json
├── routes
│ └── index.js
First, you need to set up the web server. Create a bin
directory and a www
file with the following content.
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('setup-express:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}
Next, to define routes for your API, create a routes
directory and an index.js
file with the contents below.
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/messages', function(req, res, next) {
res.send({messages: ['Hello']});
});
module.exports = router;
To validate the access token, create a middlewares
directory and an authentication.js
file with the contents below.
const jose = require('jose')
const jwksClient = jose.createRemoteJWKSet(new URL('http://localhost:9011/.well-known/jwks.json'));
const authentication = async (req, res, next) => {
const access_token = req.cookies['app.at'];
if (!access_token) {
res.status(401);
res.send({error: 'Missing token cookie and Authorization header'});
} else {
try {
await jose.jwtVerify(access_token, jwksClient, {
issuer: 'http://localhost:9011',
audience: 'e9fdb985-9173-4e01-9d73-ac2d60d1dc8e',
});
next();
} catch (e) {
if (e instanceof jose.errors.JOSEError) {
res.status(401);
res.send({error: e.message, code: e.code});
} else {
res.status(500);
res.send({error: JSON.stringify(e)});
}
}
}
}
module.exports = authentication;
To finish the API, create an app.js
file in the root directory. This will be the main entry point for your API.
require('dotenv').config(); // this loads env vars
var express = require('express');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var cors = require('cors');
var indexRouter = require('./routes/index');
const authentication = require('./middlewares/authentication');
var app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(authentication);
app.use('/', indexRouter);
module.exports = app;
Once you have created these files, you can test the API.
Now, open the terminal window and run:
npm start
Visit http://localhost:3000/messages, you'll get an error:
{"error":"Missing token cookie and Authorization header"}
Your API is protected. Now, let's get an access token so authorized clients can get the API results.
Testing the API Flow
There are a number of ways to get an access token, as mentioned, but for clarity, let's use the login API to mimic a client.
Run this command in a terminal window:
curl -H 'Authorization: YOUR_API_KEY_FROM_ABOVE' \
-H 'Content-type: application/json' \
-d '{"loginId": "YOUR_EMAIL", "password":"YOUR_PASSWORD","applicationId": "e9fdb985-9173-4e01-9d73-ac2d60d1dc8e"}' \
http://localhost:9011/api/login
Replace YOUR_EMAIL
and YOUR_PASSWORD
with the username and password you set up previously.
This request will return something like this:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0ODUxNDA5ODQsImlhdCI6MTQ4NTEzNzM4NCwiaXNzIjoiYWNtZS5jb20iLCJzdWIiOiIyOWFjMGMxOC0wYjRhLTQyY2YtODJmYy0wM2Q1NzAzMThhMWQiLCJhcHBsaWNhdGlvbklkIjoiNzkxMDM3MzQtOTdhYi00ZDFhLWFmMzctZTAwNmQwNWQyOTUyIiwicm9sZXMiOltdfQ.Mp0Pcwsz5VECK11Kf2ZZNF_SMKu5CgBeLN9ZOP04kZo",
"user": {
"active": true,
"birthDate": "1976-05-30",
"connectorId": "e3306678-a53a-4964-9040-1c96f36dda72",
"data": {
"displayName": "Johnny Boy",
"favoriteColors": [
"Red",
"Blue"
]
},
"email": "example@fusionauth.io",
"expiry": 1571786483322,
"firstName": "John",
"fullName": "John Doe",
"id": "00000000-0000-0001-0000-000000000000",
"imageUrl": "http://65.media.tumblr.com/tumblr_l7dbl0MHbU1qz50x3o1_500.png",
"lastLoginInstant": 1471786483322,
"lastName": "Doe",
"middleName": "William",
"mobilePhone": "303-555-1234",
"passwordChangeRequired": false,
"passwordLastUpdateInstant": 1471786483322,
"preferredLanguages": [
"en",
"fr"
],
"registrations": [
{
"applicationId": "10000000-0000-0002-0000-000000000001",
"data": {
"displayName": "Johnny",
"favoriteSports": [
"Football",
"Basketball"
]
},
"id": "00000000-0000-0002-0000-000000000000",
"insertInstant": 1446064706250,
"lastLoginInstant": 1456064601291,
"preferredLanguages": [
"en",
"fr"
],
"roles": [
"user",
"community_helper"
],
"username": "johnny123",
"usernameStatus": "ACTIVE"
}
],
"timezone": "America/Denver",
"tenantId": "f24aca2b-ce4a-4dad-951a-c9d690e71415",
"twoFactorEnabled": false,
"usernameStatus": "ACTIVE",
"username": "johnny123",
"verified": true
}
}
Grab the token
field (which begins with ey
). Replace YOUR_TOKEN below with that value, and run this command:
curl --cookie 'app.at=YOUR_TOKEN' http://localhost:3000/messages
Here you are placing the token in a cookie named app.at
. This is for compatibility with the FusionAuth best practices and the hosted backend.
If you want to store it in a different cookie or send it in the header, make sure you modify the middleware and restart the Express API.
This will result in the JSON below.
{"messages":["Hello"]}
Feedback
How helpful was this page?
See a problem?
File an issue in our docs repo
Have a question or comment to share?
Visit the FusionAuth community forum.