In this quickstart you are going to build an application with Express and integrate it with FusionAuth. You’ll be building it for ChangeBank, a global leader in converting dollars into coins. It’ll have areas reserved for users who have logged in as well as public facing sections.

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


General architecture

While this sample application doesn’t have login functionality without FusionAuth, a more typical integration will replace an existing login system with FusionAuth.

In that case, the system might look like this before FusionAuth is introduced.

UserApplicationView HomepageClick Login LinkShow Login FormFill Out and Submit Login FormAuthenticates UserDisplay User's Account or OtherInfoUserApplication

Request flow during login before FusionAuth

The login flow will look like this after FusionAuth is introduced.

UserApplicationFusionAuthView HomepageClick Login Link (to FusionAuth)View Login FormShow Login FormFill Out and Submit Login FormAuthenticates UserGo to Redirect URIRequest the Redirect URIIs User Authenticated?User is AuthenticatedDisplay User's Account or OtherInfoUserApplicationFusionAuth

Request flow during login after FusionAuth

In general, you are introducing FusionAuth in order to normalize and consolidate user data. This helps make sure it is consistent and up-to-date as well as offloading your login security and functionality to FusionAuth.

Getting Started

In this section, you’ll get FusionAuth up and running, and configured with the ChangeBank application.

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-web.git
cd fusionauth-quickstart-javascript-express-web

Run FusionAuth via Docker

In the root directory of the repo you’ll find a Docker compose file (docker-compose.yml) and an environment variables configuration file (.env). Assuming you have Docker installed on your machine, you can stand up FusionAuth up on your machine with:

docker compose up -d

This will start three containers, once each for FusionAuth, Postgres, and Elastic.

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

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

FusionAuth will be configured with these settings:

You can log into the FusionAuth admin UI and look around if you want, but with Docker/Kickstart you don’t need to.

Setup Application

In this section, you’ll set up a basic Express application.

Create a new directory to hold your application, and go into it.

mkdir changebank
cd changebank

The @fusionauth/typescript-client will be used to exchange the OAuth Code for Access Token using PKCE, and also to retrieve the User using JWT.

    "name": "fusionauth-quickstart-javascript-express-web",
    "version": "1.0.0",
    "description": "Example of setting up an express application with typescript.",
    "scripts": {
        "dev": "ts-node-dev --exit-child src/index.ts",
        "setup": "ts-node src/setup.ts"
    "author": "Alex Patterson",
    "license": "Apache-2.0",
    "dependencies": {
        "@fusionauth/typescript-client": "^1.46.0",
        "cookie-parser": "^1.4.6",
        "dotenv": "^16.3.1",
        "express": "^4.18.2",
        "jsonwebtoken": "^9.0.2",
        "jwks-rsa": "^3.0.1",
        "pkce-challenge": "^3.1.0"
    "devDependencies": {
        "@types/cookie-parser": "^1.4.3",
        "@types/express": "^4.17.17",
        "@types/jsonwebtoken": "^9.0.3",
        "@types/node": "^20.4.2",
        "ts-node": "^10.9.1",
        "ts-node-dev": "^2.0.0",
        "typescript": "^5.1.6"

Install dependencies using npm.

npm install

Create a new directory to hold your source code, and go into it.

mkdir src
cd src


Make sure once again that you are in the directory change-bank/src. Create a file called index.ts and start by setting up your imports and constants. These constants have already been created by running the kickstart when running FusionAuth.

import FusionAuthClient from "@fusionauth/typescript-client";
import express from 'express';
import cookieParser from 'cookie-parser';
import pkceChallenge from 'pkce-challenge';
import { GetPublicKeyOrSecret, verify } from 'jsonwebtoken';
import jwksClient, { RsaSigningKey } from 'jwks-rsa';
import * as path from 'path';

// Add environment variables
import * as dotenv from "dotenv";

const app = express();
const port = 8080; // default port to listen

if (!process.env.clientId) {
  console.error('Missing clientId from .env');
if (!process.env.clientSecret) {
  console.error('Missing clientSecret from .env');
if (!process.env.fusionAuthURL) {
  console.error('Missing clientSecret from .env');
const clientId = process.env.clientId;
const clientSecret = process.env.clientSecret;
const fusionAuthURL = process.env.fusionAuthURL;

// Validate the token signature, make sure it wasn't expired
const validateUser = async (userTokenCookie: { access_token: string }) => {
  // Make sure the user is authenticated.
  if (!userTokenCookie || !userTokenCookie?.access_token) {
    return false;
  try {
    let decodedFromJwt;
    await verify(userTokenCookie.access_token, await getKey, undefined, (err, decoded) => {
      decodedFromJwt = decoded;
    return decodedFromJwt;
  } catch (err) {
    return false;

const getKey: GetPublicKeyOrSecret = async (header, callback) => {
  const jwks = jwksClient({
    jwksUri: `${fusionAuthURL}/.well-known/jwks.json`
  const key = await jwks.getSigningKey(header.kid) as RsaSigningKey;
  var signingKey = key?.getPublicKey() || key?.rsaPublicKey;
  callback(null, signingKey);

const userSession = 'userSession';
const userToken = 'userToken';
const userDetails = 'userDetails'; //Non Http-Only with user info (not trusted)

const client = new FusionAuthClient('noapikeyneeded', fusionAuthURL);

/** Decode Form URL Encoded data */

If you want to see where the FusionAuth values came from, they can be found in the FusionAuth app (http://localhost:9011/admin). The tenant ID is found on the Tenants page. To see the Client Id and Client Secret, go to the Applications page and click the View icon under the actions for the ChangeBank application. You’ll find the Client Id and Client Secret values in the OAuth configuration section.

Static files

You need a way to serve your static files, luckily Express makes this easy with express.static. These files will include all of the CSS and images that are required for the UI of the ChangeBank application.

Make sure that you are in the directory change-bank when running these commands to create new directories and copy the css and images required from complete-application.

mkdir ../static && mkdir ../static/css && mkdir ../static/img && mkdir ../templates &&
cp ../../complete-application/static/css/changebank.css ../static/css/changebank.css && 
cp ../../complete-application/static/img/money.jpg ../static/img/money.jpg && 
cp ../../complete-application/templates/account.html ../templates/account.html && 
cp ../../complete-application/templates/home.html ../templates/home.html && 
cp ../../complete-application/templates/make-change.html ../templates/make-change.html

At the bottom of your index.ts file include the below code to serve any assets in the static directory.

app.use('/static', express.static(path.join(__dirname, '../static/')));


In order to show off your homepage you need to create a route with the base URL /. In this route check to see if userDetails cookie has any user values and correctly respond with either the home.html or account.html template. Also if there is no user you can generate the PKCE challenge pair and store them into userSession cookie (this is an HTTPOnly cookie, inaccessible to JavaScript).

In this file you will use the pkce-challenge package to generate a Proof Key for Code Exchange (PKCE) challenge pair. Read more about PKCE.

At the bottom of index.ts file include the below code, so that the homepage can be displayed when a user is not logged in. If a user is logged in this route will redirect to the /account route.

app.get("/", async (req, res) => {
  const userTokenCookie = req.cookies[userToken];
  if (await validateUser(userTokenCookie)) {
    res.redirect(302, '/account');
  } else {
    const stateValue = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
    const pkcePair = await pkceChallenge();
    res.cookie(userSession, { stateValue, verifier: pkcePair.code_verifier, challenge: pkcePair.code_challenge }, { httpOnly: true });

    res.sendFile(path.join(__dirname, '../templates/home.html'));

Login Route

In the ChangeBank application there is a Login button which will open the /login route when clicked. This route will redirect them to a login page managed by FusionAuth. Include the below code to the bottom of index.ts.

app.get('/login', (req, res, next) => {
  const userSessionCookie = req.cookies[userSession];

  // Cookie was cleared, just send back (hacky way)
  if (!userSessionCookie?.stateValue || !userSessionCookie?.challenge) {
    res.redirect(302, '/');

  res.redirect(302, `${fusionAuthURL}/oauth2/authorize?client_id=${clientId}&response_type=code&redirect_uri=http://localhost:${port}/oauth-redirect&state=${userSessionCookie?.stateValue}&code_challenge=${userSessionCookie?.challenge}&code_challenge_method=S256`)

OAuth Redirect

When FusionAuth redirects back to your Express application, it will return to your /oauth-redirect route. In this response it will have two query parameters that you need to further examine: code and state.

You’ll need to do the following:

  1. Check that the stateValue stored in the cookie matches the state query parameter.
  2. Make a request to the FusionAuth token endpoint exchanging the code, clientId, clientSecret, Redirect URI, and verifier from our userSession cookie. The response will contain the required accessToken.
  3. Retrieve the user by passing the access token to FusionAuth.
  4. Store access token details in the userToken cookie.
  5. Store retrieved user in userDetails cookie.
  6. Redirect to /account route.

Add the following code to the bottom of index.ts.

app.get('/oauth-redirect', async (req, res, next) => {
  // Capture query params
  const stateFromFusionAuth = `${req.query?.state}`;
  const authCode = `${req.query?.code}`;

  // Validate cookie state matches FusionAuth's returned state
  const userSessionCookie = req.cookies[userSession];
  if (stateFromFusionAuth !== userSessionCookie?.stateValue) {
    console.log("State doesn't match. uh-oh.");
    console.log("Saw: " + stateFromFusionAuth + ", but expected: " + userSessionCookie?.stateValue);
    res.redirect(302, '/');

  try {
    // Exchange Auth Code and Verifier for Access Token
    const accessToken = (await client.exchangeOAuthCodeForAccessTokenUsingPKCE(authCode,

    if (!accessToken.access_token) {
      console.error('Failed to get Access Token')
    res.cookie(userToken, accessToken, { httpOnly: true })

    // Exchange Access Token for User
    const userResponse = (await client.retrieveUserUsingJWT(accessToken.access_token)).response;
    if (!userResponse?.user) {
      console.error('Failed to get User from access token, redirecting home.');
      res.redirect(302, '/');
    res.cookie(userDetails, userResponse.user);

    res.redirect(302, '/account');
  } catch (err: any) {
    res.status(err?.statusCode || 500).json(JSON.stringify({
      error: err

Account Route

The /account page represents what a user sees when they’re logged into their ChangeBank account. A user that isn’t logged in who tries to access this page will just be taken to the home page.

Add the following code to the bottom of index.ts.

app.get("/account", async (req, res) => {
  const userTokenCookie = req.cookies[userToken];
  if (!await validateUser(userTokenCookie)) {
    res.redirect(302, '/');
  } else {
    res.sendFile(path.join(__dirname, '../templates/account.html'));

Make Change

Two functions are needed to accept both GET and POST requests on the /make-change route. The GET verifies that the user is logged in, then serves the HTML file required for the page. The POST takes the amount form value as a body parameter and uses the it to calculate how many quarters, dimes, nickels and pennies to return. If total is not a valid dollar value or cannot be converted an error message is returned. The return string message is received by asynchronously and updates the change-message or error-message div accordingly.

Add the following code to the bottom of index.ts.

app.get("/make-change", async (req, res) => {
  const userTokenCookie = req.cookies[userToken];
  if (!await validateUser(userTokenCookie)) {
    res.redirect(302, '/');
  } else {
    res.sendFile(path.join(__dirname, '../templates/make-change.html'));

app.post("/make-change", async (req, res) => {
  const userTokenCookie = req.cookies[userToken];
  if (!await validateUser(userTokenCookie)) {
      error: 'Unauthorized'

  let error;
  let message;

  var coins = {
    quarters: 0.25,
    dimes: 0.1,
    nickels: 0.05,
    pennies: 0.01,

  try {
    message = 'We can make change for';
    let remainingAmount = +req.body.amount;
    for (const [name, nominal] of Object.entries(coins)) {
      let count = Math.floor(remainingAmount / nominal);
      remainingAmount =
        Math.round((remainingAmount - count * nominal) * 100) / 100;

      message = `${message} ${count} ${name}`;
  } catch (ex: any) {
    error = `There was a problem converting the amount submitted. ${ex.message}`;


Logout Route

Click the Logout button and watch the browser first go to FusionAuth to log out the user, then return to your home page.

The /logout route in your application will redirect to the FusionAuth server. FusionAuth will then end the FusionAuth SSO session for this user and redirect back to the application’slogoutURL specified in the kickstarter.json file. You’ll build that URL handler below.

Add the following code to the bottom of index.ts.

app.get('/logout', (req, res, next) => {
  res.redirect(302, `${fusionAuthURL}/oauth2/logout?client_id=${clientId}`);

The application’s logout route is /oauth2/logout. Within this route we can safely clear all of the cookies related to this user.

Add the following code to the bottom of index.ts.

app.get('/oauth2/logout', (req, res, next) => {
  console.log('Logging out...')

  res.redirect(302, '/')

Listen for connection

Listen for connections on port http://localhost:8080 and serve the Express application.

Add the below code to the bottom of index.ts.

app.listen(port, () => {
  console.log(`server started at http://localhost:${port}`);

Run Application

Return to the root directory of your application /changebank by running the below command.

cd ..

Include the necessary environment variables by creating a file called .env. Include the following variables in this file.


Now that your application is complete you can run it with the below command.

npm run dev

You can now open up an incognito window and visit the Rails app at http://localhost:8080/ . Log in with the user account mentioned above, richard@example.com.

Next Steps

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

FusionAuth Customization

FusionAuth gives you the ability to customize just about everything with the user’s experience and your application’s integration. This includes


Tenant and Application Management