Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env*
.azure
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# How API routes authenticate callers: "easyauth" (Azure Container Apps
# built-in authentication) or "anonymous" (no auth, local testing only).
# When unset, `npm run dev` allows anonymous access and `npm start` rejects
# every API request.
# AUTH_MODE=

# The Llama Cloud API key.
# LLAMA_CLOUD_API_KEY=

Expand Down
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,35 @@ However, you can use the [Azure pricing calculator](https://azure.com/e/bf51ef20

### Security

> [!IMPORTANT]
> This is a sample. Review it against your own security requirements before you put real data in `./data` or expose it to users.

#### Authentication

Every API route (`/api/chat`, `/api/chat/upload`, `/api/files/*`, `/api/sandbox`, `/api/chat/config`) checks the caller before it reads files, touches the index, or calls Azure OpenAI. The `AUTH_MODE` environment variable controls the check:

| `AUTH_MODE` | Behavior |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| _unset_ | `npm run dev` allows anonymous access so local development keeps working. `npm start` and the container image reject every API request with `401`. |
| `easyauth` | Requires the `X-MS-CLIENT-PRINCIPAL-ID` header that [Azure Container Apps built-in authentication](https://learn.microsoft.com/azure/container-apps/authentication) sets for signed-in users. Only set this when built-in authentication is enabled and requires sign-in. Container Apps strips that header from client requests only when built-in authentication is on. |
| `anonymous` | No authentication. Don't use this on a public endpoint. It's only for local testing or when another gateway authenticates every request. |

#### Turn on sign-in for your deployment

A fresh `azd up` deploys an app whose API returns `401` until you turn on sign-in:

1. In the Azure portal, open the `llama-index-javascript` container app in the `rg-<env-name>` resource group.
2. Go to **Settings > Authentication > Add identity provider**, pick **Microsoft**, and let it create a new app registration. Keep **Require authentication** and **HTTP 302 Found redirect**.
3. Tell the app to trust the signed-in user headers:

```shell
az containerapp update --name llama-index-javascript --resource-group rg-<env-name> --set-env-vars AUTH_MODE=easyauth
```

`azd provision` and `azd up` reset the container's environment variables, so run step 3 again after either one. The authentication settings from step 2 stay in place.

#### Credentials

> [!NOTE]
> When implementing this template please specify whether the template uses Managed Identity or Key Vault

Expand Down
77 changes: 77 additions & 0 deletions app/api/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { NextResponse } from "next/server";

/**
* Inbound authentication gate for every API route.
*
* AUTH_MODE controls how callers are authenticated:
* - "easyauth": Azure Container Apps built-in authentication (EasyAuth) is
* enabled in front of the app. Requests must carry the
* X-MS-CLIENT-PRINCIPAL-ID header, which the platform sets for signed-in
* callers and strips from any client-supplied request.
* - "anonymous": no authentication. Only use this for local development or
* when another trusted gateway authenticates every request.
*
* When AUTH_MODE is unset, a development build (`next dev`) allows anonymous
* access for local development, while a production build (`next start`, the
* container image) fails closed and rejects every API request. Unknown values
* also fail closed.
*/
export type AuthMode = "easyauth" | "anonymous" | "unconfigured";

const PRINCIPAL_ID_HEADER = "x-ms-client-principal-id";

// `next build` inlines NODE_ENV, so a production build can't be switched back
// to the anonymous development fallback by setting NODE_ENV at runtime.
const IS_DEV_BUILD = process.env.NODE_ENV === "development";

let warned = false;

export function resolveAuthMode(
env: Record<string, string | undefined> = process.env,
): AuthMode {
const mode = env.AUTH_MODE?.trim().toLowerCase();
if (mode === "easyauth") return "easyauth";
if (mode === "anonymous") return "anonymous";
if (!mode && IS_DEV_BUILD) return "anonymous";
return "unconfigured";
}

function warnOnce(message: string) {
if (!warned) {
warned = true;
console.warn(`[auth] ${message}`);
}
}

/**
* Returns a 401 response when the request is not authenticated, or null when
* the request may proceed.
*/
export function checkAuth(headers: Headers): NextResponse | null {
const mode = resolveAuthMode();

if (mode === "anonymous") {
if (!IS_DEV_BUILD) {
warnOnce(
"AUTH_MODE=anonymous: API routes are reachable without authentication.",
);
}
return null;
}

if (mode === "easyauth" && headers.get(PRINCIPAL_ID_HEADER)) {
return null;
}

if (mode === "unconfigured") {
warnOnce(
"Rejecting API requests because authentication is not configured. " +
"Enable Azure Container Apps authentication and set AUTH_MODE=easyauth.",
);
}

return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401, headers: { "Cache-Control": "no-store" } },
);
}
8 changes: 6 additions & 2 deletions app/api/chat/config/llamacloud/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { LLamaCloudFileService } from "llamaindex";
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { checkAuth } from "../../../auth";

/**
* This API is to get config from the backend envs and expose them to the frontend
*/
export async function GET() {
export async function GET(request: NextRequest) {
const denied = checkAuth(request.headers);
if (denied) return denied;

if (!process.env.LLAMA_CLOUD_API_KEY) {
return NextResponse.json(
{
Expand Down
8 changes: 6 additions & 2 deletions app/api/chat/config/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import { checkAuth } from "../../auth";

/**
* This API is to get config from the backend envs and expose them to the frontend
*/
export async function GET() {
export async function GET(request: NextRequest) {
const denied = checkAuth(request.headers);
if (denied) return denied;

const config = {
starterQuestions: process.env.CONVERSATION_STARTERS?.trim().split("\n"),
};
Expand Down
4 changes: 4 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { initObservability } from "@/app/observability";
import { LlamaIndexAdapter, Message, StreamData } from "ai";
import { ChatMessage, Settings } from "llamaindex";
import { NextRequest, NextResponse } from "next/server";
import { checkAuth } from "../auth";
import { createChatEngine } from "./engine/chat";
import { initSettings } from "./engine/settings";
import {
Expand All @@ -19,6 +20,9 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(request: NextRequest) {
const denied = checkAuth(request.headers);
if (denied) return denied;

// Init Vercel AI StreamData and timeout
const vercelStreamData = new StreamData();

Expand Down
4 changes: 4 additions & 0 deletions app/api/chat/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { NextRequest, NextResponse } from "next/server";
import { checkAuth } from "../../auth";
import { getDataSource } from "../engine";
import { initSettings } from "../engine/settings";
import { uploadDocument } from "../llamaindex/documents/upload";
Expand All @@ -9,6 +10,9 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";

export async function POST(request: NextRequest) {
const denied = checkAuth(request.headers);
if (denied) return denied;

try {
const {
name,
Expand Down
6 changes: 5 additions & 1 deletion app/api/files/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { readFile } from "fs/promises";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
import { checkAuth } from "../../auth";
import { DATA_DIR } from "../../chat/engine/loader";

/**
* This API is to get file data from allowed folders
* It receives path slug and response file data like serve static file
*/
export async function GET(
_request: NextRequest,
request: NextRequest,
{ params }: { params: Promise<{ slug: string[] }> },
) {
const denied = checkAuth(request.headers);
if (denied) return denied;

const slug = (await params).slug;

if (!slug) {
Expand Down
4 changes: 4 additions & 0 deletions app/api/sandbox/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import { ExecutionError, Result, Sandbox } from "@e2b/code-interpreter";
import fs from "node:fs/promises";
import path from "node:path";
import { checkAuth } from "../auth";
import { saveDocument } from "../chat/llamaindex/documents/helper";

type CodeArtifact = {
Expand Down Expand Up @@ -56,6 +57,9 @@ const SUPPORTED_TEMPLATES = [
];

export async function POST(req: Request) {
const denied = checkAuth(req.headers);
if (denied) return denied;

const { artifact }: { artifact: CodeArtifact } = await req.json();

let sbx: Sandbox;
Expand Down
12 changes: 12 additions & 0 deletions middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NextRequest, NextResponse } from "next/server";
import { checkAuth } from "./app/api/auth";

export function middleware(request: NextRequest) {
return checkAuth(request.headers) ?? NextResponse.next();
}

export const config = {
matcher: "/api/:path*",
// Node.js runtime so AUTH_MODE is read at runtime, not inlined at build time.
runtime: "nodejs",
};
5 changes: 4 additions & 1 deletion next.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@
},
"transpilePackages": [
"highlight.js"
]
],
"experimental": {
"middlewareClientMaxBodySize": "50mb"
}
}
Loading
Loading