Express
This quickstart explains how to add a login page and authentication to a website hosted with Express. 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#
This Quickstart requires:
- Node.js 22 or later
- Docker 23 or later
- On macOS and Windows, one of the following container management tools:
- Docker desktop
- OrbStack (to use Orbstack for
docker composecommands after install, rundocker context use orbstack) - Podman (in the commands below, replace
dockerwithpodman)
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#
For this QuickStart, we'll use an Express app that reads and writes data to a text file:
-
Create a file named
express-app.jswith the following contents: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}`); }); -
Install dependencies:
npm install express -
Run the app:
node express-app.js -
Open the app at http://localhost:3000.
-
Replace the text area text with "Hello Express", then click the Save Changes button.
-
In a file browser or terminal, look in the directory where you created
express-app.js. You should see a new file namednote.txt, with text content "Hello Express". -
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#
Before we can add authentication to our Express app, we need a local FusionAuth instance:
-
Open your container management tool (listed in the prerequisites above).
-
Clone the GitHub repository for the FusionAuth instance you'll use with this QuickStart to your local machine:
git clone git@github.com:FusionAuth/fusionauth-quickstart-app.git -
Navigate into the cloned directory:
cd fusionauth-quickstart-app -
To start a local instance of FusionAuth, run the following command (omit the
-dflag to see all Docker logs):docker compose up -d -
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).
-
Open http://localhost:9011/admin to access the FusionAuth Admin UI.
-
Log in with these credentials:
- username:
admin@example.com - password:
password
- username:
-
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.
-
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#
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:
-
Navigate out of the
fusionauth-quickstart-appfolder, back to our Express app (the folder that containsexpress-app.jsandnote.txt):cd .. -
Create a file named
.envwith the following contents (we provided the same configuration for the local FusionAuth instance, and then viewed it in the Admin UI):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 -
Install the dependencies we'll need to authenticate with FusionAuth:
npm install passport passport-oauth2 express-session dotenvMost importantly, we're adding Passport, which handles communication with our FusionAuth instance, and dotenv, which handles reading our configuration from the
.envfile we just created. -
Replace the contents of
express-app.jswith the following code, which adds the following functionality:- imports libraries needed for authentication dependencies
- reads configuration from the
.envfile - 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
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) => { // 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}`); }); -
Take a moment to understand the calls to
passport.use,passport.serializeUser, andpassport.deserializeUser, since these are the heart of our FusionAuth integration:usetells Passport how to talk to FusionAuthserializeUserstores the current user identity as a session cookiedeserializeUserfetches the current user identity (if there is one) using the session cookie, and enables routes to accessreq.user(even though we don't use that information in this basic app)
-
That's it! Your Express app should now use FusionAuth login. Run the app:
node express-app.js -
Open the app at http://localhost:3000.
-
Log in with the following credentials:
- username:
richard@example.com - password:
password
- username:
-
Edit the text file from the safety of an authenticated Express app.
-
Log out, and find yourself back in the cold, unprivileged world of an anonymous unauthenticated 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:
-
Open the FusionAuth Admin UI at http://localhost:9011/admin.
-
Log in with the following credentials:
- username:
admin@example.com - password:
password
- username:
-
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
-
Click the Save button in the top right of the page to save your changes to self-service registration.
-
Navigate to Users -> Richard Hendricks -> Manage .
-
In the Source tab, look for the
firstNameandlastNamefields. That's what we'll use to personalize the UI for each user. In theregistrationslist, you'll see registration data for the QuickStart app registration. We'll use theidfield here to generate a unique name for each user's note. -
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.
-
Install
jsonwebtoken, so we can access FusionAuth user data from the JWT:npm install jsonwebtoken -
Import the
jsonwebtokendependency, upgradepassport.useto fetch the user's name (first and last), and update theloginroute to request the user'sprofile: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) => { // 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.
-
Open the app at http://localhost:3000.
-
Log in with the following credentials:
- username:
richard@example.com - password:
password
- username:
-
In a separate private/incognito window, open the app at http://localhost:3000.
-
Click Register User and create an account using your personal email address, name, and birthdate.
-
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#
This was a quick introduction to using FusionAuth for user authentication. To learn more about integrating FusionAuth with your product, see Get Started.
FusionAuth has many features (including Social Login, Single Sign-On, Passwordless, Multi-factor Authentication), most of which are free. This example app is a great starting point for learning about FusionAuth features.