1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Next.js の .env.local に bcrypt ハッシュを書いたら何度試してもログインが通らなかった話 — `\$` エスケープ問題

1
Last updated at Posted at 2026-06-14

経緯 — なぜ .env.local に bcrypt ハッシュを置いたか

通常、ユーザーのパスワードハッシュは users テーブル等の DB に保存するのが一般的です。今回の .env.local に直接書く方式は DB 導入前の暫定的な仮実装 として選んだもので、後のフェーズで DB(users テーブル)に移行する予定でした。

そのため、もし最初から DB で実装していれば本問題は発生しません。「個人開発で段階的に作る過程で、DB 導入前の最小構成を環境変数で組んだ結果ハマった話」としてお読みください。

起きたこと

NextAuth.js v5 + bcryptjs で管理者ログインを実装中、.env.local に bcrypt ハッシュを保存したのに、何度試してもログインが通らなくなりました。ハッシュ値は目で見て正しい、メールアドレスも合っている、開発サーバーを再起動しても変わらない。

詰まった末に切り分けを進めた結果、原因は コードでも .env.local の書式でもなく、Next.js 自身の .env 読み込みの挙動の中にあった ことが分かりました。

本記事では症状、原因、対処を順に書きます。

検証環境

項目 バージョン
Next.js 16.2.5 (Turbopack)
NextAuth.js 5.0.0-beta.31
bcryptjs 3.0.3

エラーと最初の見立て

ログイン送信時、開発サーバーのターミナルにはこれだけが出ます。

[auth][error] CredentialsSignin: Read more at https://errors.authjs.dev#credentialssignin

最初は「パスワードを間違えただけ」と思いハッシュを生成し直したのですが、何度再生成しても通らない。grep.env.local を覗くと `ADMIN_PASSWORD_HASH="$2b$10$...`` で形式は問題なさそうに見えました。

ところがコードから process.env.ADMIN_PASSWORD_HASH を出力してみると、値が 空文字 になっていました。.env.local には確かに書いたはずなのに、process.env 経由では取得できない。原因は .env.local の読み込み段階にあったのです。

原因 — @next/env$ 展開

Next.js は .env.local@next/env 経由で読み込み、内部で dotenv-expand を使って値の中の $VAR 形式を展開します。bcrypt ハッシュ $2b$10$lLoWAAQJ... の場合、$2b$10$lLoWAAQJ... がそれぞれ未定義の変数として 空文字に置き換わる ため、最終的に値全体が壊れます。

ここで気をつけたいのが、Bash や zsh とは違って クォート種別を見ない 点です。シェルでは「シングルクォート内では $ はリテラル」が常識ですが、@next/env はシングルクォートで囲んでも展開を試みます。Laravel の .env(phpdotenv)など、シェル的に動くツールに慣れていると詰まりやすい挙動です。

公式ドキュメント(Environment Variables | Next.js)の「Default Environment Variables」セクションに「$ を値に含めたい場合は \$ でエスケープする」との注意書きがあります。

解決方法

値の中の各 $\$ に置き換えます。

# ❌ どれも壊れる
ADMIN_PASSWORD_HASH="$2b$10$lLoWAAQJwm1cxoYO9u3Mienq07V7HqVoOcIFimXddAa7ivMsCoy02"
ADMIN_PASSWORD_HASH='$2b$10$lLoWAAQJwm1cxoYO9u3Mienq07V7HqVoOcIFimXddAa7ivMsCoy02'

# ✅ 正しく読まれる
ADMIN_PASSWORD_HASH='\$2b\$10\$lLoWAAQJwm1cxoYO9u3Mienq07V7HqVoOcIFimXddAa7ivMsCoy02'

エスケープした値は読み込み時に \ が外れて $ 1 文字になるので、process.env.ADMIN_PASSWORD_HASH の中身は本来の bcrypt ハッシュそのものになります。コード側で \$ を意識する必要はありません。

開発サーバーを再起動して再ログインすると、hashLength: 60 で正しく読まれ、認証が通ります。

同じ場面に出会う条件

$ を含む値全般で同じ問題が起きます。

  • bcrypt ハッシュ(本記事)
  • PostgreSQL の DSN にパスワードとして $ が含まれる
  • SendGrid API キー(SG.xxx.$xxx 形式)
  • Stripe webhook secret(whsec_$xxx)

上記いずれかを .env.local に書く時は、\$ エスケープを反射的に入れる癖をつけておくと再発を防げます。

まとめ

  • .env.local$ を含む値を書く時は、値の中の各 $\$ でエスケープ する
  • シングルクォートで囲んでも防げない。@next/env はクォート種別を見ずに $ 展開を試みる
  • 症状は「認証が通らない」など曖昧で気付きにくいので、コードから process.env の値を直接確認するのが切り分けの近道
  • bcrypt 以外でも $ を含む API キーで同じ対処が必要

検索でこの記事にたどり着いた方は、まず .env.local$\$ に置き換えて開発サーバーを再起動してみてください。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?