-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add @plainbrew/vercel-basic-auth package #7
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
base: main
Are you sure you want to change the base?
Changes from all commits
6180ca9
a6c125a
d38e254
f669f34
56f6a71
7cd6bb0
bf73e5c
8074647
b737953
ded65c7
91ee0a9
9e5073a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| dist/ |
| 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`. |
| 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" | ||
| } | ||
| } |
| 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, | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Authorization ヘッダのパースが脆く、正当な認証情報を誤判定します。 現状は 🔧 提案差分- 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return unauthorized(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| return unauthorized(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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"] | ||
| } |
| 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, | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
認証情報オプションのランタイム検証を追加してください。
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