# How to Integrate Bolt.new with Duo Security

- Tool: Bolt.new
- Difficulty: Intermediate
- Time required: 30 minutes
- Last updated: April 2026

## TL;DR

To integrate Duo Security MFA with Bolt.new, create a Web SDK application in the Duo Admin Panel, get your client ID, client secret, and API hostname, then implement the Universal Prompt redirect flow in Next.js API routes. Duo's OAuth-based Universal Prompt requires a deployed redirect URL — the full MFA flow cannot be tested in Bolt's WebContainer preview. For simpler TOTP-based MFA without a third party, consider Supabase Auth's built-in MFA.

## Adding Duo MFA to Your Bolt.new App's Login Flow

Duo Security is the leading enterprise MFA solution, protecting millions of users at companies like Cisco, Lyft, and Toyota. It adds a second authentication factor — a push notification to the Duo Mobile app, a TOTP code from any authenticator, a hardware security key, or biometric verification — after a user correctly enters their password. For Bolt.new apps serving business or security-conscious users, Duo integration signals enterprise-grade security.

Duo's modern integration method is the Universal Prompt, which uses OAuth 2.0 with PKCE. The flow works like any OAuth authorization: after password verification, your server redirects the user to Duo's hosted challenge page, the user completes their MFA verification, and Duo redirects back to your redirect_uri with a code. Your server exchanges that code (plus the PKCE verifier) for an ID token that confirms the user successfully passed MFA. This design means you never handle or store MFA secrets — Duo manages all the device enrollment and challenge logic.

The critical constraint for Bolt.new is that the Universal Prompt requires a stable, registered redirect_uri. You must register an exact URL in the Duo Admin Panel — the dynamic, ephemeral URL of Bolt's WebContainer preview cannot be registered. The pattern for Bolt development is to build all the API route logic in Bolt, deploy to Netlify or Bolt Cloud, register your deployed URL in Duo, and test the full MFA flow on the deployed site. For applications that need MFA but do not need Duo's enterprise features specifically, Supabase Auth includes built-in TOTP that works without any redirect flow and can be tested in the Bolt preview.

## Before you start

- A Duo Security account — free trial available at duo.com (requires business email)
- Access to the Duo Admin Panel to create a Web SDK application
- A deployed Bolt.new app URL (Netlify or Bolt Cloud) to register as the Duo redirect URI
- A Bolt.new project using Next.js with an existing authentication system (email/password)
- Familiarity with OAuth 2.0 PKCE flow concepts — Duo's Universal Prompt follows standard OAuth patterns

## Step-by-step guide

### 1. Create a Duo Web SDK Application

Log in to the Duo Admin Panel at admin.duosecurity.com with your Duo admin account. In the left sidebar, navigate to Applications → Protect an Application. Search for 'Web SDK' in the integration list and click 'Protect' next to it. This creates a new Duo Web SDK application and shows you the integration credentials.

You will see three key values: Client ID (your integration key, also called ikey), Client Secret (your secret key, also called skey), and API Hostname (a unique Duo domain like api-XXXXXXXX.duosecurity.com). Copy all three — you will need them for your application's environment variables.

In the application settings, find the 'Redirect URIs' section. For now, add a placeholder like https://your-app.netlify.app/api/auth/duo/callback — you will replace this with your actual deployed URL after deployment. Duo strictly enforces that the redirect_uri in authorization requests matches a registered URI exactly.

Under 'Settings' for the application, you can optionally configure the display name (shown on the Duo prompt), allowed groups, and MFA policies. For a basic integration, the defaults are fine. Save the application.

Add to your .env file: NEXT_PUBLIC_DUO_CLIENT_ID (safe for client-side code — it's a public client identifier), DUO_CLIENT_SECRET (server-side only), DUO_API_HOSTNAME (the api-XXXXXXXX.duosecurity.com domain), and DUO_REDIRECT_URI (your deployed callback URL).

```
# .env.local
NEXT_PUBLIC_DUO_CLIENT_ID=your_duo_client_id
DUO_CLIENT_SECRET=your_duo_client_secret
DUO_API_HOSTNAME=api-xxxxxxxx.duosecurity.com
DUO_REDIRECT_URI=https://your-app.netlify.app/api/auth/duo/callback
```

**Expected result:** A Duo Web SDK application exists in the Duo Admin Panel with your credentials noted. Your .env file has all four required variables. The placeholder redirect URI is registered in Duo.

### 2. Initialize the Duo Client and Build the Auth Start Route

Duo provides an official Node.js SDK (@duosecurity/duo_universal_nodejs) that handles the PKCE challenge generation, state management, and authorization URL construction. Install it via npm in your Bolt project.

Create a src/lib/duo.ts helper that initializes the Duo Client with your credentials. The Duo Client constructor takes clientId, clientSecret, apiHost, and redirectUrl. Create a new instance using environment variables — this client is used by both API routes that handle the MFA flow.

The auth start route (/api/auth/duo/start) is called after successful password authentication. It calls duo_client.generateAuthUrl() with the current user's username. This function internally generates the PKCE state and code verifier, stores the state in the session (or returns it for you to store), and returns the full authorization URL for redirecting to Duo.

Store the PKCE state and the username in the user's session (or a short-lived cookie) before redirecting. You'll need these in the callback to verify the response. The state prevents CSRF attacks — Duo includes it in the callback and you must verify it matches what you sent.

IMPORTANT: This redirect cannot be tested in Bolt's WebContainer preview because Duo will redirect back to DUO_REDIRECT_URI, which must be a publicly accessible URL. Build the routes, deploy first, register the deployed URL in Duo, then test the full flow on the deployed site.

```
// src/lib/duo.ts
import { Client } from '@duosecurity/duo_universal_nodejs';

export function createDuoClient(): Client {
  const clientId = process.env.NEXT_PUBLIC_DUO_CLIENT_ID;
  const clientSecret = process.env.DUO_CLIENT_SECRET;
  const apiHost = process.env.DUO_API_HOSTNAME;
  const redirectUrl = process.env.DUO_REDIRECT_URI;

  if (!clientId || !clientSecret || !apiHost || !redirectUrl) {
    throw new Error('Duo Security credentials are not fully configured');
  }

  return new Client({
    clientId,
    clientSecret,
    apiHost,
    redirectUrl,
  });
}

// app/api/auth/duo/start/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createDuoClient } from '@/lib/duo';

export async function POST(request: NextRequest) {
  const { username } = await request.json();
  if (!username) {
    return NextResponse.json({ error: 'username is required' }, { status: 400 });
  }

  try {
    const duo = createDuoClient();

    // Health check verifies Duo API is reachable before redirecting
    await duo.healthCheck();

    const authUrl = await duo.createAuthUrl(username);

    // Store state and username in session for callback verification
    const response = NextResponse.json({ authUrl });
    response.cookies.set('duo_pending_user', username, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      maxAge: 300, // 5 minutes to complete MFA
      sameSite: 'lax',
    });

    return response;
  } catch (err: any) {
    return NextResponse.json({ error: err.message }, { status: 500 });
  }
}
```

**Expected result:** The Duo Client initializes correctly with your credentials. The /api/auth/duo/start route generates a Duo authorization URL and stores the pending user in a cookie. Your login flow can call this endpoint after password verification to get the Duo redirect URL.

### 3. Build the Duo Callback Handler

After the user completes MFA on Duo's Universal Prompt page, Duo redirects back to your DUO_REDIRECT_URI with two URL parameters: duo_code (the authorization code) and state (the state value from the auth URL). Your callback route exchanges these for an ID token using the Duo SDK's exchangeAuthorizationCodeFor2FAResult() function.

The callback route retrieves the pending username from the session cookie, calls the Duo SDK to exchange the code, verifies the returned ID token's username matches the pending username (to prevent session fixation attacks), and if verification passes, grants the full authenticated session. If verification fails, redirect back to login with an error.

The Duo ID token exchange must happen server-side — it uses your Client Secret. Never expose the client secret or exchange tokens in client-side code. The exchangeAuthorizationCodeFor2FAResult() function handles the PKCE verification, ID token signature validation, and claim verification internally.

After successful MFA verification, clear the duo_pending_user cookie and set a full authentication session cookie (or update the existing session to mark MFA as completed). Redirect the user to their intended destination — the original page they were trying to access before being sent to MFA.

```
// app/api/auth/duo/callback/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { createDuoClient } from '@/lib/duo';

export async function GET(request: NextRequest) {
  const duoCode = request.nextUrl.searchParams.get('duo_code');
  const state = request.nextUrl.searchParams.get('state');
  const pendingUser = request.cookies.get('duo_pending_user')?.value;

  // Validate required parameters
  if (!duoCode || !state || !pendingUser) {
    return NextResponse.redirect(new URL('/login?error=mfa_incomplete', request.url));
  }

  try {
    const duo = createDuoClient();

    // Exchange code for MFA result token
    // This validates PKCE, verifies the ID token signature, and checks claims
    const result = await duo.exchangeAuthorizationCodeFor2FAResult(
      duoCode,
      pendingUser // Duo verifies the username in the ID token matches this
    );

    // Duo SDK throws if verification fails — reaching here means MFA passed
    const response = NextResponse.redirect(new URL('/dashboard', request.url));

    // Grant full authenticated session
    response.cookies.set('mfa_verified', 'true', {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 8, // 8 hour session
      sameSite: 'strict',
    });
    response.cookies.set('authenticated_user', pendingUser, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      maxAge: 60 * 60 * 8,
      sameSite: 'strict',
    });

    // Clear the pending MFA cookie
    response.cookies.delete('duo_pending_user');

    return response;
  } catch (err: any) {
    console.error('Duo MFA verification failed:', err.message);
    const response = NextResponse.redirect(
      new URL('/login?error=mfa_failed', request.url)
    );
    response.cookies.delete('duo_pending_user');
    return response;
  }
}
```

**Expected result:** The callback handler exchanges the Duo code for a verified MFA result. Successful verification grants a full authenticated session. Failed verification (user denied, wrong code, timeout) redirects to login with an error message. The full flow works on your deployed site.

### 4. Wire MFA into Your Login Flow and Deploy

With the Duo API routes in place, connect them to your existing login flow. The typical pattern is: (1) user submits email and password, (2) your existing auth route verifies credentials, (3) if credentials are correct AND the user requires MFA, call /api/auth/duo/start to get the Duo auth URL, (4) redirect the user to the Duo URL instead of granting a session immediately, (5) after Duo callback, grant the session.

For Supabase Auth users, the typical integration point is in the login page after supabase.auth.signInWithPassword() succeeds. Check if the authenticated user has MFA required (check a custom field like mfa_required in their profile), and if so, initiate the Duo flow before allowing access to protected pages.

Deploy your app: publish to Netlify or Bolt Cloud. After getting your deployed URL (e.g., https://your-app.netlify.app), go back to the Duo Admin Panel → your Web SDK application → Settings → Redirect URIs. Replace the placeholder with your exact deployed callback URL: https://your-app.netlify.app/api/auth/duo/callback. The redirect URI must match character-for-character with DUO_REDIRECT_URI in your production environment variables.

Set all four environment variables in your deployment platform: NEXT_PUBLIC_DUO_CLIENT_ID, DUO_CLIENT_SECRET, DUO_API_HOSTNAME, and DUO_REDIRECT_URI. Trigger a new deployment after adding them. Test the full login → Duo prompt → callback → dashboard flow on your deployed site.

```
// src/middleware.ts — protect routes requiring MFA
import { NextRequest, NextResponse } from 'next/server';

export function middleware(request: NextRequest) {
  const mfaVerified = request.cookies.get('mfa_verified')?.value;
  const authenticatedUser = request.cookies.get('authenticated_user')?.value;

  // Protect /dashboard routes — require completed MFA
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!mfaVerified || !authenticatedUser) {
      return NextResponse.redirect(new URL('/login', request.url));
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: ['/dashboard/:path*'],
};
```

**Expected result:** The complete MFA flow works on your deployed site: login with password → redirect to Duo → push notification or code → callback → dashboard access. Unauthenticated or MFA-incomplete users are redirected to login by the middleware.

## Best practices

- Always call duo.healthCheck() before initiating the MFA flow — if Duo's API is down, you can show a meaningful error and optionally allow emergency bypass codes rather than leaving users locked out.
- Store Duo's pending state in a server-side session or database for multi-device MFA flows (push notifications) — browser cookies are device-specific and will fail if the user approves Duo on a different device.
- Set a short maxAge (5 minutes) on the pending MFA cookie — if a user abandons the Duo prompt and returns later, they should start fresh from the login page rather than completing a stale MFA challenge.
- Register both your production URL and a staging/preview URL as redirect URIs in Duo — this allows testing on different environments without changing the registered URI each time.
- Log all Duo MFA events (success, failure, user, timestamp) to a compliance audit table — MFA logs are required evidence for SOC 2 and ISO 27001 audits.
- For simpler TOTP MFA without enterprise requirements, use Supabase Auth's built-in MFA — it supports authenticator app TOTP without any redirect flow, works in Bolt's preview, and requires no third-party account.
- Implement MFA bypass codes (stored securely, hashed in the database) as a recovery mechanism for users who lose their Duo device — never leave users permanently locked out.

## Use cases

### Two-Factor Authentication for Admin Panel

Protect an admin dashboard with Duo MFA as a mandatory second step. After admins enter their password successfully, trigger the Duo Universal Prompt before granting access to sensitive management functions. Duo's push notification makes this low-friction for the admin while providing strong security.

Prompt example:

```
Add Duo MFA to the admin login flow. After a user enters their email and password correctly, check if they are in the 'admin' role. If they are, initiate the Duo Universal Prompt instead of granting immediate access — redirect to /api/auth/duo/start which builds the Duo authorization URL. After Duo MFA is completed, the callback at /api/auth/duo/callback verifies the result token and grants the admin session. Non-admin users bypass Duo and get immediate access. Store NEXT_PUBLIC_DUO_CLIENT_ID, DUO_CLIENT_SECRET, and DUO_API_HOSTNAME in .env.
```

### Step-Up Authentication for Sensitive Actions

Trigger Duo MFA only when users attempt high-risk actions like transferring funds, exporting data, or changing account settings — not on every login. This step-up pattern reduces MFA friction for normal use while requiring verification for sensitive operations.

Prompt example:

```
Implement step-up MFA for sensitive actions using Duo. Create a context that tracks whether the current session has completed Duo verification in the last 15 minutes. When a user tries to perform a sensitive action (delete account, export data, change email), if they have not recently verified with Duo, redirect to /api/auth/duo/start with a returnUrl parameter. After successful Duo verification, redirect to the returnUrl and allow the action. Store the Duo verification timestamp in the session.
```

### Compliance-Driven MFA for All Users

Enforce Duo MFA for every user on every login to meet compliance requirements (SOC 2, HIPAA, PCI-DSS). Every successful password authentication triggers the Duo Universal Prompt, and the application session is only granted after Duo verification succeeds.

Prompt example:

```
Enforce Duo MFA for all users on every login. After any successful password authentication, always trigger /api/auth/duo/start regardless of user role. Store a pending_duo_user_id in the session before the redirect. After Duo callback completes successfully, verify the ID token and grant the full authenticated session. If the user closes Duo or cancels verification, return them to the login page with an error. Log all MFA attempts (success, failure, timestamp, user ID) to a compliance audit table in the database.
```

## Troubleshooting

### Duo Admin Panel shows 'Invalid redirect_uri' when initiating the Universal Prompt

Cause: The redirect_uri in the authorization request does not exactly match a URI registered in the Duo application's settings. Duo performs exact string matching including protocol, domain, port, and path.

Solution: In the Duo Admin Panel, open your Web SDK application → Settings → Redirect URIs. Confirm the registered URI matches your DUO_REDIRECT_URI environment variable exactly: https://your-app.netlify.app/api/auth/duo/callback. A missing trailing slash, http vs https mismatch, or different path causes rejection.

### Duo redirect works but callback throws 'State mismatch' or 'Invalid state' error

Cause: The state value generated by createAuthUrl() was not persisted correctly between the start route and the callback, or the session cookie storing the state expired before the user completed MFA.

Solution: Increase the maxAge of the duo_pending_user session cookie to at least 300 seconds (5 minutes). Ensure the cookie is set with sameSite: 'lax' so it is sent on the Duo redirect back. If users are completing Duo verification on a different device (common with push notifications), session state must be stored in a database rather than a cookie.

```
response.cookies.set('duo_pending_user', username, {
  httpOnly: true,
  secure: true,
  maxAge: 300, // 5 minutes — ensure this is long enough for user to complete MFA
  sameSite: 'lax', // 'lax' allows the cookie to be sent on cross-site redirects
});
```

### Duo health check fails with 'Cannot connect to Duo API' during development

Cause: The DUO_API_HOSTNAME is incorrect or the Duo application is not properly configured. In Bolt's WebContainer, outbound HTTPS calls to external services work, so this is typically a credential issue.

Solution: Verify DUO_API_HOSTNAME is the exact api-XXXXXXXX.duosecurity.com subdomain from your Duo application settings. This hostname is unique to your Duo account and application. Log the environment variable value in the API route to confirm it is being read correctly.

## Frequently asked questions

### Can I test Duo MFA in Bolt.new without deploying?

Partially. The API route code (credential setup, Duo client initialization, health check) can be built and verified in Bolt's preview. However, the full Duo Universal Prompt flow — which requires Duo to redirect back to your redirect_uri — cannot complete in the WebContainer because Bolt's preview URL is dynamic and cannot be registered as a valid Duo redirect URI. Deploy to Netlify or Bolt Cloud, register your deployed URL in Duo, then test the complete MFA flow.

### Is Duo Security free to use for small apps?

Duo offers a free tier for up to 10 users, which is useful for testing and small internal apps. Beyond 10 users, Duo requires a paid plan starting at the Essentials tier. Check duo.com/pricing for current pricing — Duo's plans are per-user per-month with volume discounts for larger deployments.

### Should I use Duo or Supabase Auth's built-in MFA for my Bolt.new app?

For consumer apps, startups, or apps where users are not Duo-enrolled, use Supabase Auth's built-in TOTP MFA — it works in Bolt's preview, requires no third-party account setup for users, and supports standard authenticator apps (Google Authenticator, 1Password, etc.). Use Duo when you're building for enterprise customers who already use Duo in their organization, need push notification MFA specifically, or require Duo's compliance and audit reporting features.

### Does Duo MFA work with social login (Google, GitHub) in addition to email/password?

Yes — Duo MFA is a second factor that works after any first-factor authentication, including OAuth social login. In your callback from Google or GitHub OAuth, after confirming the user's identity, initiate the Duo prompt as a mandatory second step before granting the application session. Store the OAuth user identity in the pending session before redirecting to Duo.

### What happens if a user loses their Duo device?

Implement backup codes as a recovery mechanism: generate a set of one-time codes during enrollment, hash and store them in your database, and present them as a fallback option on the Duo challenge page. Duo also supports bypass codes in the Admin Panel for emergency access. Without recovery options, users who lose their Duo device can be permanently locked out — always implement at least one recovery path.

---

Source: https://www.rapidevelopers.com/bolt-ai-integrations/duo-security
© RapidDev — https://www.rapidevelopers.com/bolt-ai-integrations/duo-security
