Zero Trust Login with Auth0

Build an Express.js app with SSO, MFA, and a Device Trust PRD.

Introduction

30 Second Summary

Every time you log into an app and get prompted for a code from your phone, you're experiencing multi-factor authentication. Behind that simple prompt is a complex system of identity verification that security teams spend months designing and product managers spend careers refining.

In this project, you will build a web application protected by enterprise-grade identity management using Auth0, configure Multi-Factor Authentication, and write a Product Requirements Document for a Device Trust feature.

What You'll Build

A simple Express.js web application with Auth0 as the Identity Provider, featuring SSO login, MFA via an authenticator app, and a written PRD for a hypothetical Device Trust feature.

By the end of this project, you'll have:

  • A running Express.js app with Single Sign-On login powered by Auth0 and the OAuth 2.0 Authorization Code Flow.
  • MFA configured and enforced using one-time passwords via Google Authenticator, demonstrating the security-vs-friction tradeoff firsthand.
  • A Device Trust PRD grounded in real technical experience and NIST SP 800-207 Zero Trust principles.
  • Secret Mission: Add Google as a social identity provider and implement conditional access logic using Auth0 Actions.

Are there any prerequisites?

No prior experience with identity management or security concepts is needed. This guide walks you through setting up every tool from scratch, including Node.js, Auth0, and Google Authenticator on your phone.

Before We Start

Before We Start...

Install Node.js and Set Up Your Project

Your Zero Trust Demo app is built with Node.js, a JavaScript runtime that lets you run server-side code.

In this step, you will install Node.js, create your project folder, and install the packages your app needs to talk to Auth0.

In this step, get ready to:
  • Install Node.js and verify it works.
  • Create a project folder and install dependencies.
  • Set up your file structure in VS Code.
Download and install Node.js

This project uses Node.js version 22.x (LTS, codename "Jod"). The main download page at nodejs.org defaults to a newer version, so you will download v22 specifically from the Node.js archive.

macOS

  • Under **Installer Packages**, click the .pkg file for macOS.
  • Open the downloaded .pkg file and follow the installer prompts. Accept the license agreement and click through each step.
  • The installer may ask for your system password to complete the installation.

Windows

  • Under **Installer Packages**, click the .msi file matching your architecture (x64 for most machines).
  • Open the downloaded .msi file and follow the installer prompts. Accept the license agreement and click through each step.
  • Keep all default settings. The installer adds Node.js to your system PATH automatically.

Once the installer finishes, open Terminal to verify the installation.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or Command Prompt (Windows) and press Enter to open it.
  • Check your Node.js version by running this command:
node --version
  • Check your npm version by running this command:
npm --version

✔️ I see version 22.x

Node.js is installed and ready. You should see something like v22.23.0 for the node version and a 10.x number for npm.

ⓧ I see a different version number

You have a different version of Node.js installed. This project requires v22.x specifically.

  • Visit the Node.js v22 archive page at https://nodejs.org/en/download/archive/v22 and download the installer for your operating system.
  • Run the installer. It will replace your current Node.js version with v22.
  • Close and reopen your Terminal, then run node --version again to confirm you see v22.x.

ⓧ Command not found

Node.js is not installed on your machine yet.

  • Go back to https://nodejs.org/en/download/archive/v22 and download the installer for your operating system (.pkg for macOS, .msi for Windows).
  • Run the installer and accept all defaults.
  • Close and reopen your Terminal, then run node --version again. You should now see v22.x.

What is Node.js?

Node.js is a runtime that lets you execute JavaScript outside of a web browser. It comes bundled with npm (Node Package Manager), which you will use to install third-party libraries for your project.

Create your project folder and install dependencies

With Node.js installed, you can create a dedicated folder for your Zero Trust Demo app and install the packages it needs.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir zero-trust-demo
cd zero-trust-demo
  • Initialize a new Node.js project by running this command:
npm init -y

What does this command do?

The npm init -y command creates a package.json file in your folder. This file tracks your project's name, version, and dependencies. The -y flag accepts all the default settings so you don't have to answer prompts.

  • Install the three packages your project needs by running this command:
npm install express express-openid-connect dotenv

What are these packages?

express is a web framework for building your server and routes.

express-openid-connect is Auth0's official middleware that handles the entire OAuth 2.0 login flow for you. It creates login, logout, and callback routes automatically.

dotenv loads environment variables from a .env file so your secrets stay out of your code.

  • Confirm your package.json was updated by running:
cat package.json

You should see express, express-openid-connect, and dotenv listed under "dependencies".

Seeing permission errors during install?

On macOS, if you see EACCES errors, try prefixing the install command with sudo and entering your system password. On Windows, try running Command Prompt as Administrator.

help me fix npm permission errors

Create the file structure in VS Code

Your project needs three files and one folder before you start writing code in the next step.

  • Switch to VS Code (it should already be open from earlier).
  • Click File in the top menu bar.
  • Click Open Folder.
  • Navigate to your Desktop, select the zero-trust-demo folder, and click Open.

Trust the authors?

VS Code may ask if you trust the authors of the files in this folder. Click Yes, I trust the authors to proceed.

  • Create the project files by running these commands in your Terminal (still inside the zero-trust-demo folder):
touch server.js .env
mkdir views
  • Confirm the files and folder were created by running:
ls

You should see server.js, .env, views/, node_modules/, package.json, and package-lock.json in the output.

What is each file for?

server.js is where your Express app code will live.

.env stores your Auth0 credentials as environment variables, keeping secrets out of your source code.

views/ is a folder for HTML templates (you will use it in a later step).

Don't see .env in VS Code's sidebar?

Files starting with a dot are hidden by default on some systems. In VS Code's Explorer panel, your .env file should still appear. If you don't see it, click the refresh icon at the top of the Explorer panel.

help me find my .env file in VS Code

Now update your package.json to match the project configuration.

  • In VS Code, click package.json in the Explorer sidebar to open it.
  • Replace the entire contents of the file with the following code:
{
  "name": "zero-trust-demo",
  "version": "1.0.0",
  "description": "Zero Trust Login Experience with Auth0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "dotenv": "16.4.7",
    "express": "4.21.2",
    "express-openid-connect": "2.20.2"
  }
}

Why update package.json manually?

The npm init -y command generates generic defaults. Updating package.json directly pins your dependency versions exactly and adds a start script so you can later run your server with npm start.

  • Save package.json with Cmd+S (macOS) or Ctrl+S (Windows).
  • In your Terminal, reinstall dependencies to match the pinned versions by running:
npm install

Your project structure is now complete and ready for coding.

✔️ Awesome, I've got everything!

Great. Double check you have saved package.json and that all three files (server.js, .env, package.json) plus the views/ folder appear in VS Code's Explorer sidebar.

ⓧ I'd like to double check the full code

Here is what your package.json should look like:

{
  "name": "zero-trust-demo",
  "version": "1.0.0",
  "description": "Zero Trust Login Experience with Auth0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "dotenv": "16.4.7",
    "express": "4.21.2",
    "express-openid-connect": "2.20.2"
  }
}

Your server.js and .env files should be empty (you will add code to them in the next step). The views/ folder should exist and be empty.

Your development environment is locked in. Next up, you will create your Auth0 application and configure it as the identity provider for your app.

Create Your Auth0 Application

Your project folder is ready with all the dependencies installed. Now you need an authentication service to handle logins for your app.

Instead of building login forms and password storage yourself, you will use Auth0 as a managed Identity Provider. Auth0 handles credential verification, token issuance, and the login UI so your app never touches user passwords.

In this step, get ready to:
  • Create a free Auth0 account and register your application.
  • Configure your application's callback and logout URLs.
  • Populate your .env file with Auth0 credentials.
Sign up for Auth0 and create your application

Auth0 needs to know about your app before it can handle logins for it. You will register your app in the Auth0 dashboard so it gets assigned a unique Client ID and secret.

  • Open your browser and navigate to auth0.com.
  • Click Sign Up and create a free account using your email or a social provider.
  • Once you are in the Auth0 Dashboard, click Applications in the left sidebar.
  • Click Applications again in the submenu that appears.
  • Click + Create Application.
  • Enter Zero Trust Demo as the application name.
  • Select Regular Web Applications as the application type.
  • Click Create.

What is an Identity Provider?

An Identity Provider (IdP) is a service that authenticates users and issues identity tokens. Instead of your app storing passwords and managing login flows, you delegate that responsibility to the IdP.

Auth0 is one of many IdPs. Others include Okta, Microsoft Entra ID, and Google Identity. The benefit is that your app receives a verified identity token without ever handling raw credentials.

Configure your application URLs

Auth0 needs to know which URLs are safe to redirect users to after login and logout. Without these, Auth0 will reject the redirect and your login flow will break.

  • After creating the application, click the Settings tab.
  • Scroll down to the Application URIs section.
  • In the Allowed Callback URLs field, enter:
  • http://localhost:3000/callback
  • In the Allowed Logout URLs field, enter:
  • http://localhost:3000
  • In the Allowed Web Origins field, enter:
  • http://localhost:3000
  • Scroll to the bottom and click Save Changes.

Why do these URLs matter?

Auth0 only redirects users to URLs you have explicitly allowlisted. The Allowed Callback URLs field tells Auth0 where to send users after they log in. The Allowed Logout URLs field tells Auth0 where to send them after they log out.

This prevents attackers from hijacking the redirect and sending users to a malicious site. It is a core security measure in the OAuth 2.0 protocol.

Don't see Application URIs?

Make sure you are on the Settings tab (not Quick Start or Connections). The Application URIs section is about halfway down the page, below the Basic Information section.

help me find the Application URIs section in my Auth0 dashboard

Copy your Auth0 credentials into the .env file

Your Express app needs several values from Auth0 to connect to it. You will copy these from the dashboard into your .env file.

  • Scroll back up to the top of the Settings tab.
  • Find the Domain, Client ID, and Client Secret fields in the Basic Information section.
  • Switch back to VS Code and open the .env file.
  • Add the following environment variables, replacing the placeholder values with your actual Auth0 credentials:
SECRET=a-long-random-string-at-least-32-characters-long
BASE_URL=http://localhost:3000
CLIENT_ID=your-auth0-client-id
ISSUER_BASE_URL=https://your-auth0-domain.auth0.com
CLIENT_SECRET=your-auth0-client-secret

What does each variable do?

  • SECRET is a random string used to encrypt your app's session cookies. Make it at least 32 characters long.
  • BASE_URL is your app's address. During development, this is always http://localhost:3000.
  • CLIENT_ID is the public identifier for your Auth0 application. Copy this directly from the dashboard.
  • ISSUER_BASE_URL is your Auth0 tenant domain (formatted as https:// plus your domain). Copy the Domain value from Auth0 and add https:// in front of it.
  • CLIENT_SECRET is a private key that proves your app's identity to Auth0. Never share this publicly or commit it to version control.
  • Replace a-long-random-string-at-least-32-characters-long with any random string you like (at least 32 characters). You can mash your keyboard to generate one.
  • Replace your-auth0-client-id with the Client ID value from your Auth0 dashboard.
  • Replace your-auth0-domain.auth0.com with the Domain value from your Auth0 dashboard.
  • Replace your-auth0-client-secret with the Client Secret value from your Auth0 dashboard.
  • Save the .env file.

Keep your Client Secret private

Never commit your .env file to Git. If you plan to push this project to GitHub later, add .env to a .gitignore file to prevent accidental exposure.

What just happened?

You have registered your app with Auth0 and established a trust relationship. Auth0 is now your Identity Provider (IdP). When users visit your app and click login, they will be redirected to Auth0's hosted login page.

Auth0 handles the entire Authorization Code Flow: it shows the login UI, verifies credentials, and issues tokens back to your app. Your application never sees or stores user passwords.

✔️ Awesome, I've got everything!

Great. Double check that your .env file has all five variables filled in with your real Auth0 values (not the placeholders).

ⓧ I'd like to double check the full code

Here is what your .env file should look like (with your own values replacing the placeholders):

SECRET=a-long-random-string-at-least-32-characters-long
BASE_URL=http://localhost:3000
CLIENT_ID=your-auth0-client-id
ISSUER_BASE_URL=https://your-auth0-domain.auth0.com
CLIENT_SECRET=your-auth0-client-secret

Your Auth0 application is configured and your app has everything it needs to connect. Next up, you will write the Express server code that uses these credentials to create a working login flow.

Build the Express App with OIDC Login

Your Auth0 application is configured and ready to authenticate users. Now it's time to build the actual Express.js app that talks to it.

In this step, you'll write a server that uses the OAuth 2.0 Authorization Code Flow to redirect users to Auth0 for login, receive them back with a token, and display their profile information. Your app never sees user passwords. Auth0 handles all of that.

In this step, get ready to:
  • Write the Express server and configure OIDC middleware.
  • Create a public homepage and a protected profile page.
  • Start the server and test the full login flow.
Configure the Express server and OIDC middleware

The express-openid-connect library is a middleware that plugs directly into Express. Once configured, it automatically creates /login, /logout, and /callback routes for you.

  • In VS Code, open server.js and add the following code to set up the server and OIDC middleware:
// Import Express and the Auth0 OIDC middleware
const express = require('express');
const { auth, requiresAuth } = require('express-openid-connect');
require('dotenv').config();

const app = express();

// Configure Auth0 connection using your .env values
const config = {
  authRequired: false,
  auth0Logout: true,
  secret: process.env.SECRET,
  baseURL: process.env.BASE_URL,
  clientID: process.env.CLIENT_ID,
  issuerBaseURL: process.env.ISSUER_BASE_URL,
};

// Attach the OIDC middleware to Express
app.use(auth(config));

What does this code do?

  • auth(config) connects your app to Auth0 using OpenID Connect. It automatically handles the Authorization Code Flow behind the scenes.
  • authRequired: false means visitors can browse public pages without logging in. You'll protect specific routes separately.
  • The middleware reads your credentials from the .env file so your secrets never appear in source code.

You won't see a change yet. The next chunk adds the routes that use this middleware.

Create the homepage and profile routes

Your app needs two pages. A public homepage that shows different content depending on whether the visitor is logged in, and a protected profile page that only authenticated users can access.

  • Add the following code below app.use(auth(config)); in server.js:
// Public homepage - shows login/logout based on auth state
app.get('/', (req, res) => {
  if (req.oidc.isAuthenticated()) {
    res.send(`
      <h1>Zero Trust Demo</h1>
      <p>Hello, ${req.oidc.user.name}!</p>
      <p><a href="/profile">View Profile</a></p>
      <p><a href="/logout">Logout</a></p>
    `);
  } else {
    res.send(`
      <h1>Zero Trust Demo</h1>
      <p>You are not logged in.</p>
      <p><a href="/login">Log in</a></p>
    `);
  }
});

// Protected profile page - requires authentication
app.get('/profile', requiresAuth(), (req, res) => {
  res.send(`
    <h1>Profile</h1>
    <p>Name: ${req.oidc.user.name}</p>
    <p>Email: ${req.oidc.user.email}</p>
    <p><a href="/">Home</a></p>
    <p><a href="/logout">Logout</a></p>
  `);
});

// Start the server
const port = 3000;
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

What does this code do?

  • req.oidc.isAuthenticated() checks if the visitor has a valid session. The homepage uses this to show either a welcome message or a login link.
  • requiresAuth() is route-level middleware that blocks unauthenticated visitors. If someone tries to access /profile without logging in, they're automatically redirected to Auth0's login page.
  • req.oidc.user contains the authenticated user's profile data (name, email) from the ID token that Auth0 issued.
  • Save server.js.

Why authRequired: false?

Setting authRequired: false at the middleware level means your app has both public and protected pages. This matches real-world apps where marketing pages are public but dashboards require login.

You then use requiresAuth() on specific routes that need protection. This gives you fine-grained control over which pages require authentication.

✔️ Awesome, I've got everything!

Great. Make sure you've saved server.js before moving on.

ⓧ I'd like to double check the full code

Here's the complete server.js file. Compare it with yours:

const express = require('express');
const { auth, requiresAuth } = require('express-openid-connect');
require('dotenv').config();

const app = express();

const config = {
  authRequired: false,
  auth0Logout: true,
  secret: process.env.SECRET,
  baseURL: process.env.BASE_URL,
  clientID: process.env.CLIENT_ID,
  issuerBaseURL: process.env.ISSUER_BASE_URL,
};

app.use(auth(config));

app.get('/', (req, res) => {
  if (req.oidc.isAuthenticated()) {
    res.send(`
      <h1>Zero Trust Demo</h1>
      <p>Hello, ${req.oidc.user.name}!</p>
      <p><a href="/profile">View Profile</a></p>
      <p><a href="/logout">Logout</a></p>
    `);
  } else {
    res.send(`
      <h1>Zero Trust Demo</h1>
      <p>You are not logged in.</p>
      <p><a href="/login">Log in</a></p>
    `);
  }
});

app.get('/profile', requiresAuth(), (req, res) => {
  res.send(`
    <h1>Profile</h1>
    <p>Name: ${req.oidc.user.name}</p>
    <p>Email: ${req.oidc.user.email}</p>
    <p><a href="/">Home</a></p>
    <p><a href="/logout">Logout</a></p>
  `);
});

const port = 3000;
app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});
Start the server and test the login flow

Your app is complete. Time to start it up and watch the full OIDC flow in action.

  • Start the server by running this command in your terminal:
node server.js
  • You should see Server running at http://localhost:3000 in the terminal.
  • Open your browser and navigate to http://localhost:3000.

You should see the homepage with a "Log in" link. The Single Sign-On flow is about to begin.

  • Click the Log in link on the page.
  • Auth0's Universal Login page appears. Click Sign Up and create an account using your email and a password.
  • After signing up, Auth0 redirects you back to your app. The homepage now greets you by name.
  • Click View Profile to see your name and email displayed on the profile page.
  • Click Logout to end the session and return to the logged-out homepage.

What just happened?

You just experienced the full Authorization Code Flow. Here's what happened behind the scenes:

  • Your app redirected the browser to Auth0 (the Identity Provider).
  • Auth0 authenticated the user and redirected back to /callback with an authorization code.
  • The middleware exchanged that code for tokens (ID token + access token) on a secure back-channel.
  • A session was created, and req.oidc.user was populated with the profile data from the ID token.

Your app never saw the user's password. Auth0 handled all credential verification.

Not seeing the login page?

  • Check that your .env file has the correct ISSUER_BASE_URL value. It should look like https://your-domain.auth0.com (with https:// at the start).
  • Verify that CLIENT_ID in .env matches the Client ID shown in your Auth0 dashboard under Applications > Zero Trust Demo > Settings.
  • Make sure your Allowed Callback URL in Auth0 is exactly http://localhost:3000/callback with no trailing slash.

Still stuck? Help me debug why my Auth0 login redirect isn't working.

Your app now handles login, logout, and profile display using enterprise-grade SSO. Next up, you'll enable Multi-Factor Authentication and experience the security-vs-friction tradeoff firsthand.

Enable MFA and Observe the UX Impact

Your app authenticates users through Auth0's Universal Login, but right now a stolen password is all an attacker needs to access the profile page.

In this step, you'll add a second layer of defense by enabling Multi-Factor Authentication. You'll experience the security-vs-friction tradeoff firsthand, which is the core product decision a PM on a Device Trust team navigates daily.

In this step, get ready to:
  • Enable OTP as an MFA factor in the Auth0 dashboard.
  • Enroll your phone with Google Authenticator and test the login flow.
  • Reflect on the UX impact of adding a second authentication step.
Enable One-time Password in the Auth0 dashboard

Right now, your app relies on a single factor (password) to verify identity. Adding a second factor means an attacker who steals a password still cannot log in without physical access to the user's phone.

  • Return to the Auth0 Dashboard in your browser.
  • In the left sidebar, click Security.
  • Click Multi-factor Auth.
  • You'll see a list of available factors. Find One-time Password and toggle it to enabled.
  • Scroll down to the Define policies section.
  • Select Always to require MFA for every login.
  • Click Save.

What is OTP?

OTP stands for One-time Password. It's a 6-digit code generated by an authenticator app (like Google Authenticator) that refreshes every 30 seconds. Because the code changes constantly and lives only on your physical device, it's much harder for an attacker to steal than a static password.

Why Always instead of Adaptive?

Auth0 offers three MFA policy options: Never, Use Adaptive MFA, and Always. For this project, you want to experience MFA on every login so you can feel the friction firsthand. In production, teams often use adaptive policies that only trigger MFA when risk signals are elevated.

Don't see the One-time Password factor?

Make sure you're on the correct page: Security in the left sidebar, then Multi-factor Auth. The OTP toggle should appear in the list of factors alongside options like Push Notification and WebAuthn.

If you don't see the page at all, confirm you're signed into the correct Auth0 tenant (the one that contains your "Zero Trust Demo" application).

Help me find the MFA settings in my Auth0 dashboard.

Enroll in MFA and test the login flow

Now you'll experience the enrollment flow as a user would. Make sure you have the Google Authenticator app installed on your phone before continuing.

  • Return to your app at http://localhost:3000 in your browser.
  • Click Logout to end your current session.
  • Click Log in to start a fresh login.
  • Enter your email and password as usual.
  • After entering credentials, Auth0 now prompts you to set up a second factor. You'll see a QR code on screen.
  • Open Google Authenticator on your phone.
  • Tap the + button in the app, then select Scan a QR code.
  • Point your phone's camera at the QR code displayed by Auth0.
  • Google Authenticator adds an entry and displays a 6-digit code. Enter this code into the Auth0 prompt.
  • Click Continue to complete enrollment.

Auth0 redirects you back to your app. You should see your profile page with your name and email, just like before.

What just happened in the login flow?

This first login after enabling MFA took three steps: enter credentials, scan the QR code to enroll, and enter the 6-digit code. Auth0 handled the entire enrollment UI for you through Universal Login.

On all future logins, the flow will be two steps: enter credentials, then enter the current 6-digit code from your authenticator app. The QR enrollment only happens once per device.

QR code not scanning?

Make sure your phone's camera has permission to scan. If the QR code still won't scan, look for a link on the Auth0 enrollment screen that says something like "Can't scan?" or an option to enter a code manually. This gives you a text secret you can type directly into Google Authenticator.

If your OTP code is rejected, check that your phone's date and time are set to automatic. TOTP codes are time-based, so a clock skew of even 30 seconds can cause rejections.

My authenticator code keeps getting rejected during MFA enrollment.

  • Log out again, then log back in one more time.
  • This time Auth0 skips the QR enrollment and goes straight to asking for the 6-digit code from your authenticator app.
  • Open Google Authenticator, find the current code, and enter it.
  • Confirm you land on the homepage with your name displayed.
Reflect on the security-vs-friction tradeoff

You just experienced the exact product tension that Zero Trust Architecture teams deal with every day. More security factors mean more friction for users.

  • Think about what you gained: even if someone steals your password, they cannot log in without your physical phone.
  • Think about what it cost: every single login now requires pulling out your phone, opening an app, and typing a 6-digit code.
  • Consider the enrollment experience: the first login took 3 steps (credentials, QR scan, code entry). Subsequent logins take 2 steps (credentials + code).

Why does this matter for a Device Trust PM?

The whole premise of Device Trust is reducing this friction for users on known, secure devices. If the system can verify that a device has disk encryption enabled, an up-to-date OS, and is managed by the company, it can skip the MFA prompt entirely.

Your firsthand experience with the friction of "Always" MFA is exactly the problem a Device Trust feature solves. You'll reference this experience directly in the PRD you write in the next step.

Your app now requires two factors for every login. In the next step, you'll channel this experience into a professional deliverable: a Device Trust PRD that proposes an intelligent solution to the friction you just felt.

Write Your Device Trust PRD

You've now felt the MFA friction firsthand. Every login requires credentials plus a one-time code from your authenticator app. That extra step is great for security, but it's a tax on productivity.

In this step, you'll channel that experience into a Product Requirements Document (PRD) for a Device Trust feature. This is the deliverable that bridges your technical understanding with product thinking, exactly what a PM on an identity team would produce.

In this step, get ready to:
  • Write a Problem Statement grounded in NIST SP 800-207 and your firsthand MFA experience.
  • Define a Proposed Solution with device posture checks and adaptive trust.
  • Set measurable Success Metrics and document tradeoffs.
Create the file and write the Problem Statement

Every good PRD starts by defining the problem clearly. Your Problem Statement will ground the reader in what's broken today and cite an authoritative framework to justify the change.

  • In VS Code, create a new file called DEVICE_TRUST_PRD.md in your zero-trust-demo project folder (the same folder that holds server.js).
  • Paste the following content into DEVICE_TRUST_PRD.md:
# Device Trust Feature - Product Requirements Document

## Problem Statement

Users of the Zero Trust Demo application currently authenticate with credentials + MFA on every login, regardless of whether they are using a trusted corporate device or an unknown personal device. This creates unnecessary friction for users on managed, secure devices while providing the same level of scrutiny to potentially compromised endpoints.

Per NIST SP 800-207 (Zero Trust Architecture), access decisions should be based on dynamic risk signals including identity, device health, and environmental context - not just credentials alone.

What does this section do?

The Problem Statement establishes two things. First, it names the pain you experienced directly: MFA on every single login regardless of device. Second, it references NIST SP 800-207 as the authoritative framework for Zero Trust Architecture.

NIST SP 800-207 defines the principle that trust should never be implicit. Access decisions should incorporate dynamic signals like device health, not just a username and password. By citing this standard, your PRD shows research depth that stands out in interviews.

  • Save DEVICE_TRUST_PRD.md.
  • Confirm the file exists by running:
ls DEVICE_TRUST_PRD.md

You should see DEVICE_TRUST_PRD.md listed in the output.

Write the Proposed Solution

The Proposed Solution section is where you define what "device trust" actually means in practice. You'll describe the posture checks, map out user flows for trusted vs untrusted devices, and introduce the concept of adaptive trust.

  • In DEVICE_TRUST_PRD.md, add the following content below the Problem Statement:
## Proposed Solution

### What is Device Trust?

Device Trust verifies that a device meets minimum security requirements before granting access to protected resources. Device posture checks include:

- Operating system version (minimum required version)
- Disk encryption enabled (FileVault on macOS, BitLocker on Windows)
- Firewall enabled
- Device management status (MDM-enrolled or not)
- Endpoint security agent running

### User Flows

**Trusted Device (managed, compliant):**
1. User enters credentials
2. System checks device posture via agent/certificate
3. Device passes posture check -> access granted (MFA skipped)

**Untrusted Device (unknown, non-compliant):**
1. User enters credentials
2. System checks device posture -> fails or no posture data
3. MFA required (OTP via authenticator app)
4. Prompt: "Enroll this device for faster access next time"

### Adaptive Trust

Device trust is not a binary yes/no decision. It is one signal among many in an adaptive access control model. Trust should be:

- Continuously re-evaluated (not just at initial login)
- Combined with other signals: user risk score, session age, location, requested resource sensitivity
- Degraded over time if posture checks are not refreshed

Why include adaptive trust?

A common mistake in Device Trust design is treating it as a binary gate: trusted or not. Real-world systems combine device posture with other risk signals (location, time, behavior) to make continuous access decisions.

Including this nuance in your PRD shows you understand that security is a spectrum, not a checkbox. This is the kind of thinking identity teams look for in PM candidates.

  • Save DEVICE_TRUST_PRD.md.

Your PRD now has the Problem Statement and a full Proposed Solution with device posture checks, user flows, and adaptive trust.

Define Success Metrics and tradeoffs

Success Metrics make your PRD actionable. Without measurable outcomes, a feature request is just a wish. You'll also document the tradeoffs and risks, which demonstrates mature product thinking.

  • In DEVICE_TRUST_PRD.md, add the following content below the Adaptive Trust section:
## Success Metrics

1. **Reduction in MFA prompts**: 80% fewer MFA challenges for users on trusted devices within 60 days of rollout
2. **Security posture maintained**: No increase in account compromise incidents (measured via security incident reports, baseline: current monthly average)
3. **Enrollment adoption**: 90% of active users complete device enrollment within 30 days of feature launch

## Tradeoffs and Risks

- **Privacy**: Device telemetry collection raises user privacy concerns, especially for BYOD. Require clear consent and data minimization.
- **BYOD complexity**: Personal devices may not meet posture requirements. Need a graceful fallback (MFA) rather than hard blocking.
- **Over-trust risk**: A device that was trusted at enrollment can be compromised later. Continuous posture reassessment and short credential lifetimes mitigate this.
- **Platform coverage**: Must support macOS, Windows, iOS, Android, and ChromeOS. Each has different posture signals available.

What makes these metrics strong?

Each metric is specific, measurable, and time-bound. "80% fewer MFA prompts within 60 days" is something an engineering team can build dashboards around. Compare that to vague goals like "improve user experience."

The tradeoffs section is equally important. It shows you've thought about what could go wrong. Privacy concerns, BYOD edge cases, and the risk of over-trusting a compromised device are all real challenges that Device Trust teams navigate.

  • Save DEVICE_TRUST_PRD.md.
  • Scroll through the full file in VS Code. You should see four major sections: Problem Statement, Proposed Solution, Success Metrics, and Tradeoffs and Risks.

PRD sections look jumbled?

Make sure each section heading (##) has a blank line above and below it. Markdown requires blank lines to separate headings from body text.

If the formatting looks off, check that you didn't accidentally delete any blank lines when pasting.

Ask for help: My DEVICE_TRUST_PRD.md formatting looks wrong in VS Code. How do I fix the Markdown structure?

✔️ Awesome, I've got everything!

Great. Make sure you've saved DEVICE_TRUST_PRD.md with all four sections in place.

ⓧ I'd like to double check the full code

Here is the complete DEVICE_TRUST_PRD.md file for comparison:

# Device Trust Feature - Product Requirements Document

## Problem Statement

Users of the Zero Trust Demo application currently authenticate with credentials + MFA on every login, regardless of whether they are using a trusted corporate device or an unknown personal device. This creates unnecessary friction for users on managed, secure devices while providing the same level of scrutiny to potentially compromised endpoints.

Per NIST SP 800-207 (Zero Trust Architecture), access decisions should be based on dynamic risk signals including identity, device health, and environmental context - not just credentials alone.

## Proposed Solution

### What is Device Trust?

Device Trust verifies that a device meets minimum security requirements before granting access to protected resources. Device posture checks include:

- Operating system version (minimum required version)
- Disk encryption enabled (FileVault on macOS, BitLocker on Windows)
- Firewall enabled
- Device management status (MDM-enrolled or not)
- Endpoint security agent running

### User Flows

**Trusted Device (managed, compliant):**
1. User enters credentials
2. System checks device posture via agent/certificate
3. Device passes posture check -> access granted (MFA skipped)

**Untrusted Device (unknown, non-compliant):**
1. User enters credentials
2. System checks device posture -> fails or no posture data
3. MFA required (OTP via authenticator app)
4. Prompt: "Enroll this device for faster access next time"

### Adaptive Trust

Device trust is not a binary yes/no decision. It is one signal among many in an adaptive access control model. Trust should be:

- Continuously re-evaluated (not just at initial login)
- Combined with other signals: user risk score, session age, location, requested resource sensitivity
- Degraded over time if posture checks are not refreshed

## Success Metrics

1. **Reduction in MFA prompts**: 80% fewer MFA challenges for users on trusted devices within 60 days of rollout
2. **Security posture maintained**: No increase in account compromise incidents (measured via security incident reports, baseline: current monthly average)
3. **Enrollment adoption**: 90% of active users complete device enrollment within 30 days of feature launch

## Tradeoffs and Risks

- **Privacy**: Device telemetry collection raises user privacy concerns, especially for BYOD. Require clear consent and data minimization.
- **BYOD complexity**: Personal devices may not meet posture requirements. Need a graceful fallback (MFA) rather than hard blocking.
- **Over-trust risk**: A device that was trusted at enrollment can be compromised later. Continuous posture reassessment and short credential lifetimes mitigate this.
- **Platform coverage**: Must support macOS, Windows, iOS, Android, and ChromeOS. Each has different posture signals available.

Why does this PRD matter for interviews?

You built the MFA login flow yourself. You felt the friction. Now you've articulated a solution grounded in a real security framework (NIST SP 800-207) with measurable outcomes.

This combination of hands-on technical experience plus product documentation is exactly what identity and Device Trust teams want to see from PM candidates. You can speak credibly about OAuth flows, MFA tradeoffs, and Zero Trust principles because you've experienced them firsthand.

Secret mission

Add a Social Identity Provider and Conditional Access Logic

You have a working SSO login with MFA. But real identity teams manage federated identity providers and programmatic access control policies. In this secret mission, you will add Google as a social login option and build an Auth0 Action that blocks access based on email domain.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally and uses Auth0's free tier, so there are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Local project folder (zero-trust-demo) on your Desktop.
  • Auth0 tenant with the "Zero Trust Demo" application, MFA configuration, Google social connection, and Login Flow Action.
  • Google Cloud Console OAuth 2.0 Client ID (created for the Secret Mission's Google social login).
  • Google Authenticator TOTP entry for Auth0 on your phone.

Keep everything running

No action needed. Choose this if you are still actively building or want to revisit the project later without any setup.

  • Your Express app stays on your Desktop and can be started again with node server.js.
  • Your Auth0 tenant remains configured and ready for future experiments.
  • Your Google Cloud Console OAuth credentials stay active.
  • Node.js remains installed on your machine.

Pause - I'll come back to this later

Shut down running processes to free up memory. Your project files and cloud configurations stay intact for when you return.

  • In your terminal, press Ctrl+C to stop the Express server if it is still running.
  • Close VS Code.
  • Your Auth0 tenant, Google Cloud OAuth credentials, and local files will all remain ready when you come back.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below to clean up completely.

Stop the server
  • In your terminal, press Ctrl+C to stop the Express server if it is still running.
Delete the Auth0 application
  • In the Auth0 Dashboard, go to Applications > Applications.
  • Click Zero Trust Demo to open it.
  • Scroll to the bottom of the Settings page and locate the Danger Zone.
  • Click Delete and confirm the deletion.
Delete the Auth0 tenant (optional)

If you no longer need your Auth0 account for other projects, you can delete the entire tenant.

  • In the Auth0 Dashboard, go to Settings and switch to the Advanced view.
  • Scroll to the bottom and locate the Danger Zone.
  • Click Delete and confirm by selecting Yes, Delete Tenant.

Tenant deletion is permanent

Once you delete a tenant, the name cannot be reused and the configuration cannot be restored. Only delete if you are sure you will not need this Auth0 setup again.

Delete the Google Cloud Console OAuth credentials
  • Open the Google Cloud Console and navigate to APIs & Services > Credentials.
  • In the OAuth 2.0 Client IDs section, check the box next to the client you created for Auth0.
  • Click Delete and confirm.
Remove the Google Authenticator entry
  • Open the Google Authenticator app on your phone.
  • Find the Auth0 entry you enrolled during the project.
  • Long-press (Android) or swipe left (iOS) on the entry and delete it.
Delete the local project folder
  • Delete the project folder by running this command:

macOS

cd ~/Desktop
rm -rf zero-trust-demo

Windows

cd ~\Desktop
rmdir /s /q zero-trust-demo
  • Confirm the folder is gone by running:
ls ~/Desktop

You should no longer see zero-trust-demo in the output.

Nice Work!

Nice Work!

You just built a production-grade login experience from scratch using Auth0, OAuth 2.0, and MFA. You now have the technical foundation and a written PRD to speak credibly about Zero Trust architecture in any PM interview for an identity or cybersecurity team.

You've learned how to:

  • Build a working Express.js web app with SSO login powered by Auth0 and the OAuth 2.0 Authorization Code Flow.
  • Configure and enforce Multi-Factor Authentication (OTP) and experience the security-vs-usability tradeoff firsthand.
  • Write a Device Trust PRD grounded in real technical experience and NIST SP 800-207 Zero Trust principles.
  • Secret Mission: Add federated identity via Google and implement conditional access logic using Auth0 Actions.

Ready to quiz yourself?