Skip to content

CORS Configuration for Public API #11512

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 21, 2025
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
20 changes: 20 additions & 0 deletions front/config/cors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const ALLOWED_ORIGINS = ["https://front-ext.dust.tt"] as const;
type AllowedOriginType = (typeof ALLOWED_ORIGINS)[number];

export function isAllowedOrigin(origin: string): origin is AllowedOriginType {
return ALLOWED_ORIGINS.includes(origin as AllowedOriginType);
}

export const ALLOWED_HEADERS = [
"authorization",
"content-type",
"x-commit-hash",
"x-dust-extension-version",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We also allow a header for hacker one researchers (see our vdp documentation)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, added.

"x-hackerone-research",
"x-request-origin",
] as const;
type AllowedHeaderType = (typeof ALLOWED_HEADERS)[number];

export function isAllowedHeader(header: string): header is AllowedHeaderType {
return ALLOWED_HEADERS.includes(header as AllowedHeaderType);
}
91 changes: 91 additions & 0 deletions front/middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { NextRequest } from "next/server";
import { NextResponse } from "next/server";

import {
ALLOWED_HEADERS,
isAllowedHeader,
isAllowedOrigin,
} from "@app/config/cors";

export function middleware(request: NextRequest) {
const url = request.nextUrl.pathname;

Expand Down Expand Up @@ -49,9 +55,94 @@ export function middleware(request: NextRequest) {
});
}

// Handle CORS only for public API endpoints.
if (url.startsWith("/api/v1")) {
if (request.method === "OPTIONS") {
// Handle preflight request.
const response = new NextResponse(null, { status: 200 });
return handleCors(response, request);
}

// Handle actual request.
const response = NextResponse.next();
return handleCors(response, request);
}

return NextResponse.next();
}

function handleCors(
response: NextResponse,
request: NextRequest
): NextResponse {
const corsResponseError = setCorsHeaders(response, request);
if (corsResponseError) {
// If setCorsHeaders returned a response, it's an error.
return corsResponseError;
}

return response;
}

function setCorsHeaders(
response: NextResponse,
request: NextRequest
): NextResponse | undefined {
const origin = request.headers.get("origin");
const requestHeaders = request.headers
.get("access-control-request-headers")
?.toLowerCase();

// If this is a preflight request checking headers.
if (request.method === "OPTIONS" && requestHeaders) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If OPTIONS and no header we should 403 as well instead of blanket 200?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding was that an OPTIONS request without CORS-specific headers is likely a regular HTTP OPTIONS request (checking which HTTP methods are supported). These are valid HTTP requests that should receive a 200 OK with appropriate Allow headers

const requestedHeaders = requestHeaders.split(",").map((h) => h.trim());
const hasUnallowedHeader = requestedHeaders.some(
(header) => !isAllowedHeader(header)
);

if (hasUnallowedHeader) {
return new NextResponse(null, {
status: 403,
statusText: "Forbidden: Unauthorized Headers",
});
}
}

// Check origin.
if (!origin) {
return new NextResponse(null, {
status: 403,
statusText: "Forbidden: Missing Origin",
});
}

// Check if origin is allowed (prod or dev).

// Cannot use helper functions like isDevelopment() in Edge Runtime middleware since they are not
// bundled. Must check NODE_ENV directly.
const isDevelopment = process.env.NODE_ENV === "development";
if (isDevelopment || isAllowedOrigin(origin)) {
response.headers.set("Access-Control-Allow-Origin", origin);
response.headers.set("Access-Control-Allow-Credentials", "true");
} else {
return new NextResponse(null, {
status: 403,
statusText: "Forbidden: Unauthorized Origin",
});
}

response.headers.set(
"Access-Control-Allow-Methods",
"GET, POST, PUT, DELETE, OPTIONS"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put OPTIONS here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See comment from above.

);
response.headers.set(
"Access-Control-Allow-Headers",
ALLOWED_HEADERS.join(", ")
);

return undefined;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

at some point the middleware should return a 403 if the origin is not allowed or if it request a non allowed header in Access-Control-Request-Headers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 ah yeah indeed, when I tried I got a 403, because I was not_authenticated but let me add a proper error handler for this.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated. Let me know if this is what you had in mind.


export const config = {
matcher: "/:path*",
};