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
- Open your Cloudflare Zero Trust Dashboard.
- Navigate to Access > Service Credentials > Service Tokens.
- Click Create Service Token.
- Name it something like NextJS-Chatbot-Widget and choose an expiration duration.
- Click Generate token.
- 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:
- In the Zero Trust dashboard, go to Access > Applications and click Edit on your Open WebUI tunnel application.
- Go to the Policies tab and click Add a Policy (or Create new policy).
- 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)
- Under Configure rules:
- Include: Select Service Token from the dropdown menu.
- Value: Choose the name of the service token you created in Step 1.
- 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 });
}
}