Skip to main content

BehindAccess

  • when openwebui is behind a cloudflare access, we need to make some changes for the ChatWidget to work

Step 1: Generate a Service Token in Cloudflare

  1. Open your Cloudflare Zero Trust Dashboard.
  2. Navigate to Access > Service Credentials > Service Tokens.
  3. Click Create Service Token.
  4. Name it something like NextJS-Chatbot-Widget and choose an expiration duration.
  5. Click Generate token.
  6. Cloudflare will show you a Client ID and a Client Secret. Copy both immediately (you will not be able to view the secret again).

Step 2: Add a Service Auth Rule to Your Application Policy

  • You need to tell Cloudflare Access that requests carrying this specific token are allowed to pass through without logging in:
  1. In the Zero Trust dashboard, go to Access > Applications and click Edit on your Open WebUI tunnel application.
  2. Go to the Policies tab and click Add a Policy (or Create new policy).
  3. Configure the policy exactly like this:
    • Policy name: Allow Chatbot API Proxy
    • Action: Service Auth (Do not pick "Allow" – "Service Auth" is required for automated tokens)
  4. Under Configure rules:
    • Include: Select Service Token from the dropdown menu.
    • Value: Choose the name of the service token you created in Step 1.
  5. Save the policy and save the application.

Step 3: Update .env

  • Add your new Cloudflare Access credentials to your local environments file:
OPEN_WEBUI_API_KEY="sk-your-actual-api-key-here"
OPEN_WEBUI_URL="https://your-public-cloudflare-domain.com"
OPEN_WEBUI_MODEL="custom-tcm-helper"

# 👇 ADD THESE TWO NEW KEYS 👇
CF_ACCESS_CLIENT_ID="your-cloudflare-service-token-client-id"
CF_ACCESS_CLIENT_SECRET="your-cloudflare-service-token-client-secret"

Step 4: Add the Headers to route.js

  • When Cloudflare receives an automated API request, it expects to see the service credentials attached as two custom tracking headers: CF-Access-Client-Id and CF-Access-Client-Secret.
  • Update your app/api/chat/route.js file to forward those headers:
import { NextResponse } from 'next/server';

export async function POST(request) {
try {
const { messages } = await request.json();

const targetUrl = `${process.env.OPEN_WEBUI_URL}/api/chat/completions`;
const apiKey = process.env.OPEN_WEBUI_API_KEY;
const modelName = process.env.OPEN_WEBUI_MODEL;

// 1. Generate live dynamic timestamps locked to Taipei timezone (CST)
const now = new Date();
const currentDate = now.toLocaleDateString('zh-TW', {
timeZone: 'Asia/Taipei',
year: 'numeric',
month: '2-digit',
day: '2-digit'
}).replace(/\//g, '-');

const currentTime = now.toLocaleTimeString('zh-TW', {
timeZone: 'Asia/Taipei',
hour12: false,
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});

const timeContextPrompt = {
role: "system",
content: `The current local date is ${currentDate} and the current time is ${currentTime}.`
};

const finalizedMessages = [timeContextPrompt, ...messages];

const sessionChatId = `external-widget-${Date.now()}`;
const syntheticMessageId = `msg-${Math.random().toString(36).substring(2, 11)}`;

const response = await fetch(targetUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,

// 👇 ADD CLOUDFLARE ACCESS BYPASS HEADERS HERE 👇
'CF-Access-Client-Id': process.env.CF_ACCESS_CLIENT_ID || '',
'CF-Access-Client-Secret': process.env.CF_ACCESS_CLIENT_SECRET || '',
},
body: JSON.stringify({
model: modelName,
messages: finalizedMessages,
stream: false,
chat_id: sessionChatId,
id: syntheticMessageId,
parent_id: "root"
}),
});

if (!response.ok) {
const errorData = await response.text();
console.error('Open WebUI Error Status:', response.status, errorData);
return NextResponse.json({ error: 'LLM Server returned an error' }, { status: response.status });
}

const data = await response.json();
return NextResponse.json(data);
} catch (error) {
console.error('Failed to proxy chat route:', error);
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}