Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/vercel-basic-auth/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/
52 changes: 52 additions & 0 deletions packages/vercel-basic-auth/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# @plainbrew/vercel-basic-auth

Basic Auth handler for Vercel Edge Middleware.

## Install

```sh
pnpm add @plainbrew/vercel-basic-auth
```

## Usage

`proxy.ts`:

```ts
import { basicAuth } from "@plainbrew/vercel-basic-auth";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

export default async function proxy(request: NextRequest) {
const basicAuthResponse = basicAuth(request, {
username: process.env.BASIC_AUTH_USER ?? "",
password: process.env.BASIC_AUTH_PASSWORD ?? "",
// vercelEnvTarget: "all", // Apply Basic Auth to all Vercel environments
// dev: true, // Apply Basic Auth in local development
});
if (basicAuthResponse) return basicAuthResponse;

return NextResponse.next();
}
```

## Options

| Option | Type | Required | Default | Description |
| ----------------- | --------- | -------- | ------------------- | ------------------------------------------ |
| `username` | `string` | ✓ | | Basic Auth username |
| `password` | `string` | ✓ | | Basic Auth password |
| `vercelEnvTarget` | `string` | | `'only-production'` | Vercel environments to apply Basic Auth |
| `dev` | `boolean` | | `false` | Apply Basic Auth in `NODE_ENV=development` |

### `vercelEnvTarget`

| Value | Behavior |
| ----------------- | --------------------------------------------- |
| `only-production` | Apply Basic Auth to Vercel production only |
| `all` | Apply Basic Auth to all Vercel environments |
| `disabled` | Disable Basic Auth on all Vercel environments |

### Notes

- Basic Auth is only applied on Vercel (`VERCEL=1`) by default. Local development is skipped unless `dev: true`.
38 changes: 38 additions & 0 deletions packages/vercel-basic-auth/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@plainbrew/vercel-basic-auth",
"version": "0.0.0",
"description": "Basic Auth handler for Vercel Edge Middleware",
"keywords": [
"basic-auth",
"middleware",
"vercel"
],
"license": "MIT",
"files": [
"dist"
],
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": {
"types": "./dist/index.d.mts",
"default": "./dist/index.mjs"
},
"require": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"scripts": {
"dev": "tsup --watch",
"build": "tsup"
},
"devDependencies": {
"@types/node": "^22.0.0",
"tsup": "^8.0.0",
"typescript": "^5.0.0"
}
}
71 changes: 71 additions & 0 deletions packages/vercel-basic-auth/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
export type VercelEnvTarget = "only-production" | "all" | "disabled";

export type BasicAuthOptions = {
username: string;
password: string;
/**
* Vercel 環境のどの範囲で Basic 認証を適用するか
* @default 'only-production'
*/
vercelEnvTarget?: VercelEnvTarget;
/**
* NODE_ENV=development でも Basic 認証を適用するか
* @default false
*/
dev?: boolean;
};

export function basicAuth(
request: Request,
{
username: authUsername,
password: authPassword,
vercelEnvTarget = "only-production",
dev = false,
}: BasicAuthOptions,
Comment on lines +20 to +25
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

認証情報オプションのランタイム検証を追加してください。

username / password は必須仕様ですが、現在は空文字や不正値を実行時に弾いていません。設定ミスを早期失敗させた方が安全です。

🔧 提案差分
 export function basicAuth(
   request: Request,
   {
     username: authUsername,
     password: authPassword,
     vercelEnvTarget = "only-production",
     dev = false,
   }: BasicAuthOptions,
 ): Response | null {
+  if (!authUsername || !authPassword) {
+    throw new TypeError("basicAuth: username and password are required");
+  }
+
   function unauthorized() {
     return new Response("Auth required", {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vercel-basic-auth/src/index.ts` around lines 20 - 25, Add runtime
validation for the BasicAuthOptions inputs by checking that authUsername and
authPassword are non-empty strings (e.g., typeof === "string" and .trim().length
> 0) at the start of the exported initializer/middleware that receives {
username: authUsername, password: authPassword, vercelEnvTarget, dev }. If
either value is missing/empty/invalid, throw a clear Error (or fail-fast)
describing which credential is invalid so misconfiguration fails early; keep
vercelEnvTarget and dev handling unchanged.

): Response | null {
function unauthorized() {
return new Response("Auth required", {
status: 401,
headers: {
"WWW-Authenticate": "Basic",
},
});
}

if (process.env.NODE_ENV === "development") {
if (!dev) {
return null;
}
}

if (process.env.VERCEL === "1") {
if (vercelEnvTarget === "disabled") {
return null;
}
if (vercelEnvTarget === "only-production" && process.env.VERCEL_ENV !== "production") {
return null;
}
}

const authorization = request.headers.get("authorization");
if (!authorization) {
return unauthorized();
}

const authValue = authorization.split(" ")[1];
if (authValue === undefined) {
return unauthorized();
}

try {
const [username, password] = atob(authValue).split(":");
if (username !== authUsername || password !== authPassword) {
Comment on lines +56 to +63
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Authorization ヘッダのパースが脆く、正当な認証情報を誤判定します。

現状は split(" ") / split(":") 依存のため、Basic スキーム未検証かつ password に : を含むケースを正しく扱えません。Basic <base64> を厳密に検証し、最初の : でのみ分割してください。

🔧 提案差分
-  const authValue = authorization.split(" ")[1];
-  if (authValue === undefined) {
+  const matched = authorization.match(/^Basic\s+(.+)$/i);
+  if (!matched) {
     return unauthorized();
   }

   try {
-    const [username, password] = atob(authValue).split(":");
+    const decoded = atob(matched[1]);
+    const separatorIndex = decoded.indexOf(":");
+    if (separatorIndex < 0) {
+      return unauthorized();
+    }
+    const username = decoded.slice(0, separatorIndex);
+    const password = decoded.slice(separatorIndex + 1);
     if (username !== authUsername || password !== authPassword) {
       return unauthorized();
     }
   } catch {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const authValue = authorization.split(" ")[1];
if (authValue === undefined) {
return unauthorized();
}
try {
const [username, password] = atob(authValue).split(":");
if (username !== authUsername || password !== authPassword) {
const matched = authorization.match(/^Basic\s+(.+)$/i);
if (!matched) {
return unauthorized();
}
try {
const decoded = atob(matched[1]);
const separatorIndex = decoded.indexOf(":");
if (separatorIndex < 0) {
return unauthorized();
}
const username = decoded.slice(0, separatorIndex);
const password = decoded.slice(separatorIndex + 1);
if (username !== authUsername || password !== authPassword) {
return unauthorized();
}
} catch {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/vercel-basic-auth/src/index.ts` around lines 56 - 63, The
Authorization header parsing is brittle: ensure the header uses the Basic scheme
and extract only the base64 payload, then decode and split credentials on the
first ':' only. Replace the current authorization.split(" ")[1] logic with a
check that authorization.trim().startsWith("Basic "), obtain the substring after
the first space as authValue, decode with atob(authValue), and split the decoded
string using the first indexOf(':') to separate username and password (so
passwords containing ':' are preserved); keep the existing unauthorized() call
when validation fails and validate against authUsername/authPassword as before.

return unauthorized();
}
} catch {
return unauthorized();
}

return null;
}
14 changes: 14 additions & 0 deletions packages/vercel-basic-auth/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ES2019",
"module": "ESNext",
"moduleResolution": "Bundler",
"lib": ["ES2019", "DOM"],
"strict": true,
"declaration": true,
"esModuleInterop": true,
"skipLibCheck": true,
"types": ["node"]
},
"include": ["src"]
}
8 changes: 8 additions & 0 deletions packages/vercel-basic-auth/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
format: ["cjs", "esm"],
dts: true,
clean: true,
});
Loading