🐡

Next.js x Vercel でBasic認証を実装

2023/02/14に公開1

TL;DR

  • Next.js x Vercel で Basic 認証を middleware で実装する方法を紹介します。

https://github.com/hayato94087/next-basic-auth

動機

  • 作成しているリリース前のサイトに Basic 認証を設定したいと考えます。
  • yarn dev でも Basic 認証を求められると面倒なので、Production 環境だけ適用します。

実装方法

Basic 認証はmiddlewareを利用し実装できます。

公式のサンプルコードとして以下が参考になります。

https://vercel.com/templates/next.js/basic-auth-password

https://github.com/vercel/examples/tree/main/edge-middleware/basic-auth-password

重要なファイルは 2 つあります。

middleware.ts

以下が、middleware.ts のサンプルソースコードです。

  • //index に一致する URL に対してこの middleware 関数が適用されます。
  • middleware 関数は、authorization ヘッダーから Basic 認証情報を取得し、ユーザー名とパスワードが正しい場合は指定のアドレスにアクセスします。
  • 正しくない場合は、/api/auth を呼び出し、Basic 認証をリライトします。
middleware.ts
import { NextRequest, NextResponse } from 'next/server'

export const config = {
  matcher: ['/', '/index'],
}

export function middleware(req: NextRequest) {
  const basicAuth = req.headers.get('authorization')
  const url = req.nextUrl

  if (basicAuth) {
    const authValue = basicAuth.split(' ')[1]
    const [user, pwd] = atob(authValue).split(':')

    if (user === '4dmin' && pwd === 'testpwd123') {
      return NextResponse.next()
    }
  }
  url.pathname = '/api/auth'

  return NextResponse.rewrite(url)
}

pages/api/auth.ts

以下が、api/auth.ts のサンプルソースコードです。

  • Next.js で API ルートを作成します。
  • Basic 認証に関連する値を response に設定します。
pages/api/auth.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.setHeader('WWW-authenticate', 'Basic realm="Secure Area"')
  res.statusCode = 401
  res.end(`Auth Required.`)
}

では、次に新規にプロジェクトを作成し、実際に Basic 認証を追加実装します。

新規プロジェクト作成

terminal
$ pnpm create next-app next-basic-auth --typescript --eslint --src-dir --import-alias "@/*" --use-pnpm

実施結果

terminal
Library/pnpm/store/v3/tmp/dlx-35818      |   +1 +
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/hayato94087/Library/pnpm/store/v3
  Virtual store is at:             Library/pnpm/store/v3/tmp/dlx-35818/node_modules/.pnpm
Library/pnpm/store/v3/tmp/dlx-35818      | Progress: resolved 1, reused 1, downloaded 0, added 1, done
✔ Would you like to use experimental `app/` directory with this project? … No / Yes
Creating a new Next.js app in /Users/hayato94087/Private/next-basic-auth.

Using pnpm.

Installing dependencies:
- react
- react-dom
- next
- @next/font
- typescript
- @types/react
- @types/node
- @types/react-dom
- eslint
- eslint-config-next

Packages: +267
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Packages are hard linked from the content-addressable store to the virtual store.
  Content-addressable store is at: /Users/hayato94087/Library/pnpm/store/v3
  Virtual store is at:             node_modules/.pnpm
Progress: resolved 279, reused 261, downloaded 6, added 267, done

dependencies:
+ @next/font 13.1.6
+ @types/node 18.13.0
+ @types/react 18.0.28
+ @types/react-dom 18.0.10
+ eslint 8.34.0
+ eslint-config-next 13.1.6
+ next 13.1.6
+ react 18.2.0
+ react-dom 18.2.0
+ typescript 4.9.5

Done in 9.1s

Initializing project with template: default

Initialized a git repository.

Success! Created next-basic-auth at /Users/hayato94087/Private/next-basic-auth
<!-- textlint-disable -->

middleware.tsの作成

  • src/middleware.ts を作成します。
  • 今回はすべてのリクエストに対して Basic 認証を実施するため、matcher で特定パスを指定しません。
  • 本番環境だけ Basic 認証を適用させます。
src/middleware.ts
import { NextRequest, NextResponse } from 'next/server';

export function middleware(req: NextRequest) {
  const basicAuth = req.headers.get('authorization');
  const url = req.nextUrl;

  if (process.env.NODE_ENV === 'production') {
    if (basicAuth) {
      const authValue = basicAuth.split(' ')[1];
      const [user, pwd] = atob(authValue).split(':');

      if (user === '4dmin' && pwd === 'testpwd123') {
        return NextResponse.next();
      }
    }
    url.pathname = '/api/auth';

    return NextResponse.rewrite(url);
  }
}

実装する上での注意点があります。middleware の仕様が変更になっているため、インターネット上にある古い記事では Buffer.from で実装しているソースコードがあることに注意してください。現在、Buffer.from の代わりに atob を利用します。

以下が middleware.ts で利用できる API です。

https://nextjs.org/docs/api-reference/edge-runtime

src/pages/api/auth.ts

src/pages/api/auth.ts を作成します。

src/pages/api/auth.ts
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(_: NextApiRequest, res: NextApiResponse) {
  res.setHeader('WWW-authenticate', 'Basic realm="Secure Area"')
  res.statusCode = 401
  res.end(`Auth Required.`)
}

ローカルで動作確認

開発環境を実行します。

terminal
$ yarn dev
  • 開発環境では、process.env.NODE_ENVdevelopment になります。
  • if 文により、Basic 認証を実施せずにページを表示できます。

ローカルの本番環境で動作確認

本番環境を実行します。

terminal
$ yarn build
$ yarn start
  • 本番環境では、process.env.NODE_ENVproduction になります。
  • if 文により、Basic 認証を実施してページを表示できます。

Vercelで動作確認

以下を実施した上で、動作確認を行います。

  • GitHub のリポジトリにソースを push
  • Vercel で新規プロジェクトを作成し、GitHub のリポジトリを指定

index ページにアクセスすると、Basic 認証が実施されます。

ちなみに、画像ファイルの next.svg に直接アクセスしても Basic 認証が実施されます。

まとめ

Vercel の middleware を利し、Production 環境だけ Basic 認証を実施できました。

参考

https://zenn.dev/canesro/articles/41ca9c6c49b9fb
https://github.com/vercel/examples/tree/main/edge-functions/basic-auth-password
https://nextjs.org/docs/api-reference/edge-runtime
https://zenn.dev/monicle/articles/vercel-plain-middleware-basic-auth

Discussion

ojisanojisan

こんにちは!いつも記事拝見させていただいております。
一点、Basic認証なのでまあいいかなー、とは思いつつ、
本来以下の部分はenvであるべきかなと思ったのでコメントさせていただきました。

      if (user === '4dmin' && pwd === 'testpwd123') {
        return NextResponse.next();
      }