> For the complete documentation index, see [llms.txt](https://fusionauth.io/docs/llms.txt)

# FusionAuth Quickstart for Express | FusionAuth Docs

Integrate FusionAuth with an example Express website.

# FusionAuth Quickstart for Express

[Edit on GitHub](https://github.com/FusionAuth/fusionauth-site/blob/main/astro/src/content/docs/get-started/quickstarts/web/express.mdx)

[View Markdown](https://fusionauth.io/docs/get-started/quickstarts/web/express.md)

This quickstart explains how to add a login page and authentication to a website hosted with [Express](https://expressjs.com/). You'll begin with a simple Express app that allows visitors to edit a file. Then, you'll add authentication to the app with a FusionAuth instance. Finally, you'll extend the app to handle multiple independent users with user data provided by FusionAuth.

This tutorial should take between 5 and 15 minutes; less if you already have prerequisites like Docker installed.

## Prerequisites[#](#prerequisites)

This Quickstart requires:

*   [Node.js](https://nodejs.org/en/download) 22 or later
*   [Docker](https://docs.docker.com/get-started/get-docker/) 23 or later
*   On macOS and Windows, one of the following container management tools:
    *   [Docker desktop](https://www.docker.com/products/docker-desktop/)
    *   [OrbStack](https://docs.orbstack.dev/quick-start) (to use Orbstack for `docker compose` commands after install, run `docker context use orbstack`)
    *   [Podman](https://podman.io/docs/installation) (in the commands below, replace `docker` with `podman`)

Be sure to open your container management tool to configure and install any dependencies needed by that tool, including Rosetta and Developer Tools on macOS.

## Create a Simple Express App[#](#create-a-simple-express-app)

For this QuickStart, we'll use an Express app that reads and writes data to a text file:

1.  Create a file named `express-app.js` with the following contents:
    
    ```javascript
    const express = require('express');
    const fs = require('fs');
    const path = require('path');
    const app = express();
    const PORT = 3000;
    const TARGET_FILE = path.join(__dirname, 'note.txt');
    
    // Middleware to parse form data
    app.use(express.urlencoded({ extended: true }));
    
    // Ensure the file exists so we don't crash
    if (!fs.existsSync(TARGET_FILE)) {
        fs.writeFileSync(TARGET_FILE, 'Hello! Edit this text.', 'utf8');
    }
    
    app.get('/', (req, res) => {
        const content = fs.readFileSync(TARGET_FILE, 'utf8');
    
        // Simple HTML UI
        const html = `
            <!DOCTYPE html>
            <html>
            <head>
                <title>Text Editor</title>
                <style>
                    body { font-family: sans-serif; margin: 40px; line-height: 1.6; }
                    textarea { width: 100%; height: 300px; padding: 10px; margin-bottom: 10px; }
                    button { padding: 10px 20px; background: #007bff; color: white; border: none; cursor: pointer; }
                    button:hover { background: #0056b3; }
                </style>
            </head>
            <body>
                <h1>Edit: ${path.basename(TARGET_FILE)}</h1>
                <form method="POST" action="/save">
                    <textarea name="content">${content}</textarea>
                    <br>
                    <button type="submit">Save Changes</button>
                </form>
            </body>
            </html>
        `;
        res.send(html);
    });
    
    app.post('/save', (req, res) => {
        const newContent = req.body.content;
        fs.writeFileSync(TARGET_FILE, newContent, 'utf8');
        res.redirect('/'); // Refresh the page to show updated content
    });
    
    app.listen(PORT, () => {
        console.log(`Server running at http://localhost:${PORT}`);
    });
    ```
    
2.  Install dependencies:
    
    ```shell-session
    $ npm install express
    ```
    
3.  Run the app:
    
    ```shell-session
    $ node express-app.js
    ```
    
4.  Open the app at [http://localhost:3000](http://localhost:3000).
    
5.  Replace the text area text with "Hello Express", then click the **Save Changes** button.
    
6.  In a file browser or terminal, look in the directory where you created `express-app.js`. You should see a new file named `note.txt`, with text content "Hello Express".
    
7.  Congratulations, you have a working Express app that lets you edit a local text file through your web browser. Press **Ctrl+C** on your keyboard to stop the Express server.
    

## Install and Run FusionAuth[#](#install-and-run-fusionauth)

Before we can add authentication to our Express app, we need a local FusionAuth instance:

1.  Open your container management tool (listed in the prerequisites above).
    
2.  Clone the [GitHub repository for the FusionAuth instance you'll use with this QuickStart](https://github.com/FusionAuth/fusionauth-quickstart-app) to your local machine:
    
    ```shell-session
    $ git clone git@github.com:FusionAuth/fusionauth-quickstart-app.git
    ```
    
3.  Navigate into the cloned directory:
    
    ```shell-session
    $ cd fusionauth-quickstart-app
    ```
    
4.  To start a local instance of FusionAuth, run the following command (omit the `-d` flag to see all Docker logs):
    
    ```shell-session
    $ docker compose up -d
    ```
    
5.  Wait until all networks, volumes, and containers have a green status of **Healthy**, **Started**, or **Created** (this may take a few minutes, depending on your network speed and cached dependencies).
    
6.  Open [http://localhost:9011/admin](http://localhost:9011/admin) to access the FusionAuth Admin UI.
    
7.  Log in with these credentials:
    
    *   username: `admin@example.com`
    *   password: `password`
8.  Navigate to Applications -> QuickStart App -> Edit -> OAuth . On this page, you can find your client ID, your client secret, and specify redirect and logout URLs. In the next part of this QuickStart, we'll use these values to connect our Express app with FusionAuth.
    
9.  Click the Logout button in the top right to log out of the Admin UI. We'll use a different user account to access our Express app, and because FusionAuth supports SSO, we need to log out of our admin account before we can log in as a normal user.
    

## Add FusionAuth to the Express App[#](#add-fusionauth-to-the-express-app)

Now that you have a working instance of FusionAuth and a working Express app, let's use FusionAuth to add login to your Express app:

1.  Navigate out of the `fusionauth-quickstart-app` folder, back to our Express app (the folder that contains `express-app.js` and `note.txt`):
    
    ```shell-session
    $ cd ..
    ```
    
2.  Create a file named `.env` with the following contents (we provided the same configuration for the local FusionAuth instance, and then viewed it in the Admin UI):
    
    ```plaintext
    FUSIONAUTH_CLIENT_ID=f0510a74-da7a-4101-a474-05e7f1d5ba7e
    FUSIONAUTH_CLIENT_SECRET=super-secret-secret-that-should-be-regenerated-for-production
    FUSIONAUTH_CALLBACK_URL=http://localhost:3000/oauth-callback
    SESSION_SECRET=a_very_long_random_string
    ```
    
3.  Install the dependencies we'll need to authenticate with FusionAuth:
    
    ```shell-session
    $ npm install passport passport-oauth2 express-session dotenv
    ```
    
    Most importantly, we're adding [Passport](https://www.npmjs.com/package/passport), which handles communication with our FusionAuth instance, and [dotenv](https://www.npmjs.com/package/dotenv), which handles reading our configuration from the `.env` file we just created.
    
4.  Replace the contents of `express-app.js` with the following code, which adds the following functionality:
    
    *   imports libraries needed for authentication dependencies
    *   reads configuration from the `.env` file
    *   configures Passport to communicate with FusionAuth using a URL, client Id, client secret, and callback URL used after Oauth login
    *   uses middleware to redirect users who aren't authenticated to the login URL
    *   adds routes for login, logout, and the oauth callback that completes login
    
    ```javascript
    const express = require('express');
    const fs = require('fs');
    const path = require('path');
    const app = express();
    const PORT = 3000;
    const TARGET_FILE = path.join(__dirname, 'note.txt');
    
    // auth dependencies
    const session = require('express-session');
    const passport = require('passport');
    const OAuth2Strategy = require('passport-oauth2');
    
    // read configuration from .env file
    require('dotenv').config();
    const FUSIONAUTH_URL = 'http://localhost:9011';
    const CLIENT_ID = process.env.FUSIONAUTH_CLIENT_ID;
    const CLIENT_SECRET = process.env.FUSIONAUTH_CLIENT_SECRET;
    const CALLBACK_URL = process.env.FUSIONAUTH_CALLBACK_URL;
    
    // tell passport how to talk to fusionauth
    passport.use('fusionauth', new OAuth2Strategy({
        authorizationURL: `${FUSIONAUTH_URL}/oauth2/authorize`,
        tokenURL: `${FUSIONAUTH_URL}/oauth2/token`,
        clientID: CLIENT_ID,
        clientSecret: CLIENT_SECRET,
        callbackURL: CALLBACK_URL,
        pkce: true,
        state: true
    }, (accessToken, refreshToken, profile, cb) => {
        // In a real app, you'd verify the user here.
        // For this utility, we'll just return a generic user object.
        return cb(null, { id: 'user' });
    }));
    
    // remember users via the session cookie
    passport.serializeUser((user, done) => done(null, user));
    passport.deserializeUser((obj, done) => done(null, obj));
    
    // require authentication to access
    app.use(express.urlencoded({ extended: true }));
    app.use(session(
      { secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: true }));
    app.use(passport.initialize());
    app.use(passport.session());
    
    const ensureAuthenticated = (req, res, next) => {
        if (req.isAuthenticated()) return next();
        res.redirect('/login');
    };
    
    // Ensure the file exists so we don't crash
    if (!fs.existsSync(TARGET_FILE)) {
        fs.writeFileSync(TARGET_FILE, 'Hello! Edit this text.', 'utf8');
    }
    
    // --- Routes ---
    app.get('/login', passport.authenticate('fusionauth'));
    
    app.get('/oauth-callback',
        passport.authenticate('fusionauth', { failureRedirect: '/login' }),
        (req, res) => res.redirect('/')
    );
    
    app.get('/logout', (req, res, next) => {
      // Clear the local Express session
      req.logout((err) => {
        if (err) return next(err);
    
        // Redirect to FusionAuth to clear the SSO session
        // This ensures the user is actually logged out of the identity provider
        const fusionAuthLogout =
            `${FUSIONAUTH_URL}/oauth2/logout?client_id=${process.env.FUSIONAUTH_CLIENT_ID}`;
        res.redirect(fusionAuthLogout);
      });
    });
    
    app.get('/', ensureAuthenticated, (req, res) => {
        const content = fs.readFileSync(TARGET_FILE, 'utf8');
    
        // Simple HTML UI
        const html = `
            <!DOCTYPE html>
            <html>
            <head>
                <title>Text Editor</title>
                <style>
                    body { font-family: sans-serif; margin: 40px; line-height: 1.6; }
                    textarea { width: 100%; height: 300px; padding: 10px; margin-bottom: 10px; }
                    button { padding: 10px 20px; background: #007bff;
                             color: white; border: none; cursor: pointer; }
                    button:hover { background: #0056b3; }
                    .logout-btn { background: #dc3545; color: white; padding: 8px 15px;
                                  text-decoration: none; border-radius: 4px; float: right; }
                </style>
            </head>
            <body>
                <h1>Edit: ${path.basename(TARGET_FILE)}</h1>
                <form method="POST" action="/save">
                    <textarea name="content">${content}</textarea>
                    <br>
                    <button type="submit">Save Changes</button>
                    <a href="/logout" class="logout-btn">Logout</a>
                </form>
                <br>
            </body>
            </html>
        `;
        res.send(html);
    });
    
    app.post('/save', (req, res) => {
        const newContent = req.body.content;
        fs.writeFileSync(TARGET_FILE, newContent, 'utf8');
        res.redirect('/'); // Refresh the page to show updated content
    });
    
    app.listen(PORT, () => {
        console.log(`Server running at http://localhost:${PORT}`);
    });
    ```
    
5.  Take a moment to understand the calls to `passport.use`, `passport.serializeUser`, and `passport.deserializeUser`, since these are the heart of our FusionAuth integration:
    
    *   `use` tells Passport how to talk to FusionAuth
    *   `serializeUser` stores the current user identity as a session cookie
    *   `deserializeUser` fetches the current user identity (if there is one) using the session cookie, and enables routes to access `req.user` (even though we don't use that information in this basic app)
6.  That's it! Your Express app should now use FusionAuth login. Run the app:
    
    ```shell-session
    $ node express-app.js
    ```
    
7.  Open the app at [http://localhost:3000](http://localhost:3000).
    
8.  Log in with the following credentials:
    
    *   username: `richard@example.com`
    *   password: `password`
9.  Edit the text file from the safety of an authenticated Express app.
    
10.  Log out, and find yourself back in the cold, unprivileged world of an anonymous unauthenticated user.
     

## Add Independent Notes for Each User[#](#add-independent-notes-for-each-user)

To see a more realistic example of FusionAuth usage, we can improve the app even more, so every user gets a unique note to edit. To accomplish this, we'll use FusionAuth's user data:

1.  Open the FusionAuth Admin UI at [http://localhost:9011/admin](http://localhost:9011/admin).
    
2.  Log in with the following credentials:
    
    *   username: `admin@example.com`
    *   password: `password`
3.  Navigate to Applications -> QuickStart App -> Edit -> Registration .
    
    *   Under **Self-service registration**, toggle the **Enabled** switch to the active state.
    *   In the **Registration Fields** list, tick the **Enabled** and **Required** columns for the following rows:
        *   **Birthdate**
        *   **First name**
        *   **Last name**
4.  Click the **Save** button in the top right of the page to save your changes to self-service registration.
    
5.  Navigate to Users -> Richard Hendricks -> Manage .
    
6.  In the **Source** tab, look for the `firstName` and `lastName` fields. That's what we'll use to personalize the UI for each user. In the `registrations` list, you'll see registration data for the QuickStart app registration. We'll use the `id` field here to generate a unique name for each user's note.
    
7.  Click the Logout button in the top right to log out of the Admin UI. We'll use a different user account to access our Express app.
    
8.  Install `jsonwebtoken`, so we can access FusionAuth user data from the JWT:
    
    ```shell-session
    $ npm install jsonwebtoken
    ```
    
9.  Import the `jsonwebtoken` dependency, upgrade `passport.use` to fetch the user's name (first and last), and update the `login` route to request the user's `profile`:
    
    ```javascript
    const express = require('express');
    const fs = require('fs');
    const path = require('path');
    const jwt = require('jsonwebtoken');
    const app = express();
    const PORT = 3000;
    
    // set up a directory for multi-user files
    const DATA_DIR = path.join(__dirname, 'user_data');
    if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR);
    
    // auth dependencies
    const session = require('express-session');
    const passport = require('passport');
    const OAuth2Strategy = require('passport-oauth2');
    
    // read configuration from .env file
    require('dotenv').config();
    const FUSIONAUTH_URL = 'http://localhost:9011';
    const CLIENT_ID = process.env.FUSIONAUTH_CLIENT_ID;
    const CLIENT_SECRET = process.env.FUSIONAUTH_CLIENT_SECRET;
    const CALLBACK_URL = process.env.FUSIONAUTH_CALLBACK_URL;
    
    // tell passport how to talk to fusionauth
    passport.use('fusionauth', new OAuth2Strategy({
        authorizationURL: `${FUSIONAUTH_URL}/oauth2/authorize`,
        tokenURL: `${FUSIONAUTH_URL}/oauth2/token`,
        clientID: CLIENT_ID,
        clientSecret: CLIENT_SECRET,
        callbackURL: CALLBACK_URL,
        pkce: true,
        state: true,
        scope: ['openid', 'profile', 'email']
    }, (accessToken, refreshToken, params, profile, cb) => {
        const idToken = params.id_token;
        const decoded = jwt.decode(idToken);
    
        const firstName = decoded.given_name || '';
        const lastName = decoded.family_name || '';
        const fullName = `${firstName} ${lastName}`.trim();
    
        const user = {
           id: decoded.sub,
           email: decoded.email,
           name: fullName || decoded.email || 'User'
        };
    
        return cb(null, user);
    }));
    
    // remember users via the session cookie
    passport.serializeUser((user, done) => done(null, user));
    passport.deserializeUser((obj, done) => done(null, obj));
    
    // require authentication to access
    app.use(express.urlencoded({ extended: true }));
    app.use(session({
        secret: process.env.SESSION_SECRET,
        resave: false,
        saveUninitialized: true
    }));
    app.use(passport.initialize());
    app.use(passport.session());
    
    const ensureAuthenticated = (req, res, next) => {
        if (req.isAuthenticated()) return next();
        res.redirect('/login');
    };
    
    // helper to get specific user file
    const getUserFile = (id) => path.join(DATA_DIR, `note-${id}.txt`);
    
    // --- routes ---
    app.get('/login', passport.authenticate('fusionauth', {
        scope: ['openid', 'profile', 'email']
    }));
    
    app.get('/oauth-callback',
        passport.authenticate('fusionauth', { failureRedirect: '/login' }),
        (req, res) => res.redirect('/')
    );
    
    app.get('/logout', (req, res, next) => {
        // Clear the local Express session
        req.logout((err) => {
            if (err) return next(err);
            req.session.destroy(() => {
                // Redirect to FusionAuth to clear the SSO session
                const logoutUrl = `${FUSIONAUTH_URL}/oauth2/logout?client_id=${CLIENT_ID}` +
                    `&post_logout_redirect_uri=${encodeURIComponent('http://localhost:3000/login')}`;
                res.redirect(logoutUrl);
            });
        });
    });
    
    app.get('/', ensureAuthenticated, (req, res) => {
        const TARGET_FILE = getUserFile(req.user.id);
    
        // Ensure the file exists so we don't crash
        if (!fs.existsSync(TARGET_FILE)) {
            fs.writeFileSync(TARGET_FILE, `Hello ${req.user.name}!`, 'utf8');
        }
    
        const content = fs.readFileSync(TARGET_FILE, 'utf8');
    
        // Simple HTML UI
        const html = `
            <!DOCTYPE html>
            <html>
            <head>
                <title>Text Editor</title>
                <style>
                    body { font-family: sans-serif; margin: 40px; line-height: 1.6; }
                    textarea { width: 100%; height: 300px; padding: 10px; margin-bottom: 10px; }
                    button { padding: 10px 20px; background: #007bff;
                             color: white; border: none; cursor: pointer; }
                    button:hover { background: #0056b3; }
                    .logout-btn { background: #dc3545; color: white; padding: 8px 15px;
                                  text-decoration: none; border-radius: 4px; float: right; }
                </style>
            </head>
            <body>
                <a href="/logout" class="logout-btn">Logout</a>
                <p style="float: right; margin-right: 20px;">Welcome, ${req.user.name}</p>
                <h1>Personal Note Editor</h1>
                <form method="POST" action="/save">
                    <textarea name="content">${content}</textarea>
                    <br>
                    <button type="submit">Save Changes</button>
                </form>
            </body>
            </html>
        `;
        res.send(html);
    });
    
    app.post('/save', ensureAuthenticated, (req, res) => {
        const TARGET_FILE = getUserFile(req.user.id);
        const newContent = req.body.content;
        fs.writeFileSync(TARGET_FILE, newContent, 'utf8');
        res.redirect('/'); // Refresh the page to show updated content
    });
    
    app.listen(PORT, () => {
        console.log(`Server running at http://localhost:${PORT}`);
    });
    ```
    
    This also updates the login route and adds the profile scope to the OAuth authorization call, which provides the user's info to our app.
    
10.  Open the app at [http://localhost:3000](http://localhost:3000).
     
11.  Log in with the following credentials:
     
     *   username: `richard@example.com`
     *   password: `password`
12.  *In a separate private/incognito window*, open the app at [http://localhost:3000](http://localhost:3000).
     
13.  Click Register User and create an account using your personal email address, name, and birthdate.
     
14.  Enjoying using your unique user note! You should see your name at the top of the page, and in the default note text.
     

Your Express app now provides unique notes for each user. Each user can independently edit and save their note. To see all of the notes, look for `note-<id>.txt` files in the `user_data` directory. To identify which note corresponds to which user, check the `id` for the QuickStart app in the `registrations` section of the user data object in the User's **Source** tab.

## Next steps[#](#next-steps)

This was a quick introduction to using FusionAuth for user authentication. To learn more about integrating FusionAuth with your product, see [Get Started](http:///docs/get-started).

FusionAuth has many features (including [Social Login](https://fusionauth.io/docs/lifecycle/authenticate-users/identity-providers.md#social-identity-providers), [Single Sign-On](https://fusionauth.io/docs/lifecycle/authenticate-users/single-sign-on.md), [Passwordless](https://fusionauth.io/docs/lifecycle/authenticate-users/passwordless/webauthn-passkeys.md), [Multi-factor Authentication](https://fusionauth.io/docs/lifecycle/authenticate-users/multi-factor-authentication.md)), most of which are free. This example app is a great starting point for learning about FusionAuth features.