Vercelの公開URLに、自前でBasic認証をかけてみた話

Vercelの公開URLに、自前でBasic認証をかけてみた話

Vercelのproduction domainに、Next.js Middlewareを使った自前のBasic認証を実装してみました。Vercel標準のDeployment Protectionとの使い分けや、実装時の注意点をまとめます。
2026.09.02

夏の終わりが近づいてきましたが、まだまだ暑い日が続きますね。残暑に負けないよう麦茶を切らさないようにしている、リテールアプリ共創部所属の haruka です。

最近、新しいシステムを作るプロジェクトに関わっています。その中で、Vercelにデプロイした画面を社外の関係者にも見てもらいたい一方、誰でも見られる状態にはしたくない、という要件がありました。

VercelにはDeployment Protectionという標準機能があります。今回はその機能も検討したうえで、Vercel Proで追加アドオンを契約せず、production domainを共有パスワードで保護するために、Next.jsのMiddlewareでBasic認証を実装しました。

この記事では、Vercel標準の保護機能との違い、自前Basic認証を選んだ条件、実装時と運用時の注意点をまとめます。

この記事のコード例はNext.js 15以前を対象にしています。Next.js 16以降では、原則として同じ認証ロジックをproxy.tsへ移し、関数名をproxyへ変更すれば動きます。詳しくはNext.js 16以降の場合で説明します。

この記事で実現したいこと

今回の要件は次のとおりです。

  • フレームワーク: Next.js 15以前(Next.js 16以降では、原則として同じ認証ロジックをproxy.tsに移すことになる)
  • デプロイ先: Vercel
  • 保護対象: production domainの全パス
  • 共有相手: Vercelプロジェクトのメンバーではない社外関係者も含む
  • 認証方式: 共有するユーザー名・パスワードによるBasic認証
  • 前提: 実データ、個人情報、認証情報など、機密性の高い情報は画面に含めない

この記事でいうproduction domainは、利用者に案内する本番用のドメインです。たとえば、以下が該当します。

  • example.com
  • www.example.com
  • your-project.vercel.app

Preview URLや、Vercelがデプロイごとに発行するURLは、本記事の自前Basic認証ではなく、VercelのDeployment Protectionで保護する前提です。

先に結論

今回の構成では、保護の役割を次のように分けました。

対象 保護方法
Preview URL・デプロイごとのURL VercelのStandard Protection
production domain Next.js Middlewareによる自前Basic認証

VercelのStandard Protectionは、production domainを除くデプロイメントをVercel Authenticationで保護します。一方、production domainまでVercel標準機能で保護するには、All Deploymentsを選ぶか、Password Protectionを使います。

Password ProtectionはEnterpriseプランに含まれます。ProプランではAdvanced Deployment Protectionアドオンが必要で、執筆時点では月額150ドルです。

今回は、次の条件から自前実装を選びました。

  • production domainを固定URLとして共有したい
  • 社外の確認者へ、Vercelアカウントやプロジェクト権限の付与を求めたくない
  • ProプランでAdvanced Deployment Protectionの追加費用はかけない
  • 保護対象に高い機密性がないことをチームで確認済みである

Enterpriseプランを利用している場合、またはProで追加アドオンを利用できる場合は、Vercel標準のPassword Protectionも有力な選択肢です。自前実装が常に優れているわけではなく、要件・コスト・運用負荷に応じて選ぶことが重要です。

Basic認証で十分と判断した理由

Basic認証は、強固な認証方式ではありません。

たとえば、次のような制約があります。

  • 多要素認証を提供しない
  • 共有パスワードを使う場合、利用者ごとの権限管理や失効ができない
  • 総当たり試行へのレート制限やアカウントロックは、この実装だけでは提供しない
  • HTTPSを前提にしなければ、認証情報を安全に扱えない

そのためBasic認証は、ログイン機能や認可の代わりではなく、機密性が低い検証環境・デモ環境に対する簡易的なアクセス制限として扱う必要があります。

今回のプロジェクトでは、公開する画面に実データや個人情報を含めず、「URLを偶然知った第三者からのアクセスを防ぐ」ことが主な目的でした。チームでリスクを確認したうえで、Basic認証を採用しています。

個人情報、顧客情報、管理画面、決済情報などを扱う場合は、Basic認証だけに依存せず、組織の認証基盤との連携、多要素認証、IP制限、アプリケーション側の認証・認可なども検討するのがおすすめです。

まず検討したこと: Deployment Protection

Vercelには、デプロイメントへのアクセスを制御するDeployment Protectionがあります。主な方法は次のとおりです。

方法 概要
Vercel Authentication Vercelで認証済みで、チームまたはプロジェクトにアクセスできる利用者を許可する。Shareable Linkでの共有もできる
Password Protection 設定した共有パスワードを入力した利用者を許可する
Trusted IPs 指定したIPアドレスまたはCIDRからのアクセスだけを許可する

Vercel Authenticationは全プランで利用できます。Password ProtectionはEnterpriseプラン、またはProのAdvanced Deployment Protectionアドオンで利用できます。Trusted IPsはEnterprise向けの機能です。(執筆時:2026/09)

Vercel Authenticationが今回の用途に合わなかった理由

Vercel Authenticationは、チーム内でPreview URLを安全に共有するには便利です。社外の人にも、デプロイ単位でShareable Linkを発行して共有できます。

ただし今回は、Preview URLではなく、固定のproduction domainを社外関係者に共有したいことが主な要件でした。確認者ごとにVercelアカウントやプロジェクト権限を管理する運用は採りませんでした。

また、Standard Protectionはproduction domainを保護対象に含めません。そのため、Standard Protectionを使うだけでは、本番用の固定URLを保護できません。

Password Protectionという選択肢もある

Enterpriseプランを利用している場合、またはProでAdvanced Deployment Protectionを契約する場合は、Vercel標準のPassword Protectionが有力な選択肢です。

認証画面や認証状態の管理をVercelへ任せられるため、今回のような要件でも、標準機能で完結できる場合があります。

一方で今回は、Proプランで追加アドオンを使わない前提だったため、自前のBasic認証を選びました。

実装方針: production domainだけでBasic認証を有効にする

VercelのStandard Protectionを有効にしている場合、Preview URLやデプロイごとのURLはVercel Authenticationによって先に保護されます。

ただし、自前のMiddlewareを全ホストへ一律適用すると、Vercel Authenticationを通過したPreview URLにも、さらにBasic認証がかかります。今回の目的はproduction domainの保護なので、Basic認証を適用するホスト名を環境変数で明示することにしました。

BASIC_AUTH_HOSTS=example.com,www.example.com,your-project.vercel.app
BASIC_AUTH_USER=demo-user
BASIC_AUTH_PASS=十分に長くランダムなパスワード

BASIC_AUTH_HOSTS には、Basic認証をかけたいproduction domainをカンマ区切りで設定します。

BASIC_AUTH_USERBASIC_AUTH_PASS にはASCII文字を使う運用にしています。以下の実装ではBasic認証ヘッダーの組み立てにbtoa()を使うためです。日本語などの非ASCII文字を認証情報に使いたい場合は、文字コードを意識したBase64エンコード処理を別途実装する必要があります。

また、認証情報の環境変数名に NEXT_PUBLIC_ は付けません。NEXT_PUBLIC_ で始まる環境変数は、クライアント向けJavaScriptへ埋め込まれます。認証情報をこの形式で定義すると、ブラウザから読める状態になってしまいます。

middleware.tsを作成する

プロジェクトルート、またはsrc配下でapp・pagesと同じ階層に、middleware.ts を配置します。

// middleware.ts
import { NextResponse, type NextRequest } from 'next/server';

export const config = {
  // HTML、JavaScript、CSS、画像、APIなどを含め、全パスを対象にする。
  matcher: '/:path*',
};

function getTargetHosts(): string[] {
  return (process.env.BASIC_AUTH_HOSTS ?? '')
    .split(',')
    .map((host) => host.trim().toLowerCase())
    .filter(Boolean);
}

function isTargetHost(hostname: string): boolean {
  return getTargetHosts().includes(hostname.toLowerCase());
}

// 長さが一致する場合は、最後まで比較する。
function safeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;

  let diff = 0;
  for (let i = 0; i < a.length; i += 1) {
    diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }

  return diff === 0;
}

function serviceUnavailable(message: string): Response {
  return new Response(`${message}\n`, {
    status: 503,
    headers: {
      'content-type': 'text/plain; charset=utf-8',
      'cache-control': 'no-store',
    },
  });
}

function unauthorized(): Response {
  return new Response('Authentication required\n', {
    status: 401,
    headers: {
      'WWW-Authenticate': 'Basic realm="restricted", charset="UTF-8"',
      'content-type': 'text/plain; charset=utf-8',
      // 認証前の401レスポンスをキャッシュしない
      'cache-control': 'no-store',
    },
  });
}

export default function middleware(request: NextRequest): Response {
  const targetHosts = getTargetHosts();

  // 対象ホストの設定を忘れた場合、認証なしで公開するのではなく停止する。
  if (targetHosts.length === 0) {
    return serviceUnavailable(
      'Basic auth is not configured. Set BASIC_AUTH_HOSTS.',
    );
  }

  // Preview URLなど、production domain以外はVercelのDeployment Protectionへ任せる。
  if (!isTargetHost(request.nextUrl.hostname)) {
    return NextResponse.next();
  }

  const user = process.env.BASIC_AUTH_USER;
  const pass = process.env.BASIC_AUTH_PASS;

  // production domainでは、認証情報が未設定なら必ず停止する。
  if (!user || !pass) {
    return serviceUnavailable(
      'Basic auth is not configured. Set BASIC_AUTH_USER / BASIC_AUTH_PASS.',
    );
  }

  const expected = `Basic ${btoa(`${user}:${pass}`)}`;
  const given = request.headers.get('authorization') ?? '';

  if (safeEqual(given, expected)) {
    return NextResponse.next();
  }

  return unauthorized();
}

Next.js Middlewareは、ルートが処理される前にリクエストを扱えます。matcher を使うと実行対象のパスを指定できます。

なぜ静的アセットも認証対象にするのか

Next.jsの公式ドキュメントには、_next/static や画像ファイルなどをMiddlewareの対象外にする例があります。一般的なルーティングや性能のためには合理的な設定です。

ただし、認証を目的に matcher を設計する場合は、性能最適化のための除外設定をそのままコピーしない方が安全です。

今回の目的は「HTMLだけに認証画面を出す」ことではなく、production domainへのアクセスを原則として認証付きにすることでした。そのため、HTML、JavaScript、CSS、画像、APIを個別に除外せず、全パスを認証対象にしています。

もちろん、クライアントへ配信するJavaScriptや画像に秘密情報を含めてはいけません。Basic認証の有無にかかわらず、ブラウザへ送るデータは利用者に取得される前提で設計する必要があります。

また、全リクエストでMiddlewareが動く構成は、アクセス量に応じて利用状況へ影響します。アクセス数が多いサービスへ適用する場合は、Vercelの利用状況とトラフィック量を事前に確認しておくと安心です。

文字列比較で少しだけ気を付けたこと

認証ヘッダーの比較では、単純な === ではなく safeEqual() を使っています。

単純な比較処理では、不一致を見つけた時点で処理が終わる実装になることがあります。その場合、入力値と正解値の一致状況によって処理時間に差が生じ、理論上は秘密情報の推測につながる可能性があります。MITREでは、この種の問題を CWE-208 として整理しています。

今回の safeEqual() は、文字列長が同じ場合に最後まで比較する小さな対策です。ただし、これでBasic認証そのものが強固な認証方式になるわけではありません。Basic認証の採用条件を限定し、HTTPSで利用することが前提です。

Vercelへ環境変数を登録する

production環境へ環境変数を登録します。

vercel env add BASIC_AUTH_HOSTS production
vercel env add BASIC_AUTH_USER production
vercel env add BASIC_AUTH_PASS production --sensitive

BASIC_AUTH_PASS はSensitiveとして登録することをおすすめします。

Sensitiveな環境変数は、作成後に元の値を読み出せない形式で保存されます。値を復元することはできませんが、新しい値へ更新することは可能です。

そのため、登録前にユーザー名・パスワードを、チームで定めたパスワードマネージャーなどの安全な保管場所へ記録しておくことが重要です。

パスワードを忘れた場合は、既存の値を取得しようとするのではなく、新しい値へ更新します。

vercel env update BASIC_AUTH_PASS production --sensitive

環境変数の変更は、すでに存在するデプロイには自動反映されません。変更後は新しいproduction deploymentを作成しましょう。

デプロイして確認する

環境変数を設定したら、productionへデプロイします。

vercel --prod

認証情報なしでproduction domainへアクセスすると、401が返ります。

curl -i https://example.com/
HTTP/2 401
www-authenticate: Basic realm="restricted", charset="UTF-8"
cache-control: no-store
content-type: text/plain; charset=utf-8

正しいユーザー名・パスワードを付けると、アプリケーションのレスポンスを取得できます。

curl -i -u demo-user:<パスワー> https://example.com/

ブラウザでproduction domainを開くと、Basic認証のダイアログが表示されます。正しい認証情報を入力すれば、アプリケーションが表示されます。

あわせて、次も確認しておくと安心です。

  • example.com で401が返ること
  • www.example.com も使う場合、同様に401が返ること
  • your-project.vercel.app も公開対象なら、BASIC_AUTH_HOSTS に含めたうえで401が返ること
  • Preview URLではVercelのDeployment Protectionが動くこと
  • 認証情報を付けずにAPIエンドポイントへアクセスしても401になること
  • パスワード変更・再デプロイ後、旧パスワードでアクセスできないこと

運用上の注意

パスワードは定期的に変更する

Basic認証は共有パスワード方式です。共有相手が増えた場合や、共有が不要になった場合は、パスワードを変更しましょう。

vercel env update BASIC_AUTH_PASS production --sensitive
vercel --prod

ドメインを追加したらBASIC_AUTH_HOSTSも更新する

この実装は、BASIC_AUTH_HOSTS に含まれないホストをBasic認証なしでそのまま通します。production domainにカスタムドメインなどを追加したときに BASIC_AUTH_HOSTS への追加を忘れると、そのドメインはStandard Protectionの対象外(production domainのため)かつBasic認証の対象外にもなり、無認証で公開されてしまいます。ドメインを追加する際は、必ず BASIC_AUTH_HOSTS も一緒に更新してください。

認証情報をURLへ含めない

認証情報をURLへ埋め込む形式は避けましょう。

https://user:password@example.com/

ブラウザ履歴、アクセスログ、画面共有、エラー通知などへ認証情報が残るおそれがあります。共有する場合は、URLと認証情報を別の経路で渡します。

Basic認証を最終防衛線にしない

今回の実装は、機密性が低いコンテンツを簡易的に保護するためのものです。

アプリケーション内にAPI、管理機能、個人情報、顧客情報などがある場合は、Basic認証を通過した後にも、アプリケーション側で適切な認証・認可を実装するようにしましょう。

Next.js 16以降の場合

Next.js 16では、middleware.ts は非推奨となり、proxy.ts へ名称変更されています。

今回のBasic認証の実装であれば、認証ロジックや matcher はそのままで構いません。基本的には、次の2点を変更します。

  1. ファイル名を middleware.ts から proxy.ts へ変更する
  2. 関数名を middleware から proxy へ変更する
// proxy.ts
import { NextResponse, type NextRequest } from 'next/server';

// config、補助関数はmiddleware.ts版と同じ

export default function proxy(request: NextRequest): Response {
  // middleware.ts版と同じ認証ロジック
  return NextResponse.next();
}

default export を使う場合でも関数名自体は任意ですが、Next.jsの推奨に合わせて proxy へそろえています。

ただし、Next.js 16の proxy.ts はNode.js runtimeで動作し、Edge runtimeは指定できません。今回のコードはEdge runtime固有のAPIへ依存していないため、通常はリネームだけで問題ありません。一方、既存の middleware.tsruntime: 'edge' を明示している場合や、Middleware名を含む next.config.* の設定を使っている場合は、アップグレードガイドとcodemodの対象を確認しておくと安心です。

まとめ

Vercelのproduction domainを社外の関係者にも共有したい場合、Vercel標準のDeployment Protectionと自前Basic認証には、それぞれ向く条件があります。

今回の判断は次のとおりでした。

  • Preview URL・デプロイごとのURLはVercelのStandard Protectionで保護する
  • production domainはNext.js MiddlewareでBasic認証をかける
  • Basic認証を使うのは、実データや個人情報を含まない、機密性が低い用途に限定する
  • ProでAdvanced Deployment Protectionの追加コストをかけない条件では、自前実装も選択肢になる
  • Enterprise利用時、またはProで追加アドオンを使える場合は、Vercel標準のPassword Protectionも有力な選択肢になる
  • Next.js 16以降では、原則として同じロジックをproxy.tsへ移せばよい

「認証をかける」こと自体ではなく、誰に共有するのか、何を守るのか、どの程度の運用負荷とコストを許容するのかを整理して選ぶことが大切だと感じました。

同じように、Vercel上のアプリケーションを社外の関係者へ共有したい方の参考になれば幸いです。

参考資料

この記事をシェアする

関連記事