リバースプロキシ下で Next.js (v15) + Auth.js (v5) OAuth 認証を使う設定
はじめに
Next.js でWebアプリケーション開発をしています。
認証機能に Auth.js による Github/Twitter OAuth 認証を使用しています
デプロイ先としては、Next.js 開発元である Vercel とするのが最も簡単そうです......が、選択肢がそれしかないのは寂しいですから、レンタルサーバ中で Docker (Compose) を使用し、Nginxリバースプロキシ下(サブディレクトリ)で運用しています
サーバやドメイン(https://example.com)を一つしか持っていなくても、複数のNext.jsアプリケーション(next1, next2)を
https://example.com/next1-
https://example.com/next2
で待ち受けることが可能です
この条件で Next.js + Auth.js を動かす設定には一癖あり、随分時間が掛かりました。この記事では、Next.js (v15 App Router) + Auth.js (v5) をリバースプロキシ下(サブディレクトリ)で動かすため試行錯誤した結果をまとめます。
Nginx リバースプロキシ設定
https://example.com/girls-side-analysis へのアクセスを、Next.js アプリケーション(コンテナ名:girls-side-analysis-nextjs、ポート番号: 3000)に転送する設定です
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $upstream_cache_status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=girls_side_analysis_cache:10m inactive=60m use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
map $upstream_http_cache_control $bypass_cache {
"~(no-cache|no-store|private)" 1;
default 0;
}
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html/;
location /girls-side-analysis {
proxy_pass http://girls-side-analysis-nextjs:3000;
proxy_set_header Host $host;
# without this, server actions raises errors like cors?
# "x-forwarded-host is not matched", something like that.
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Cookie $http_cookie;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_redirect off;
proxy_cache_bypass $bypass_cache;
proxy_cache girls_side_analysis_cache;
proxy_cache_valid 200 5m;
proxy_cache_use_stale error timeout updating;
add_header X-Proxy-Cache $upstream_cache_status;
}
}
}
proxy_pass設定について重要な選択肢があります。 上記の設定ファイルでは
location /girls-side-analysis {
proxy_pass http://girls-side-analysis-nextjs:3000; # 末尾の "/" なし
}
としており、この設定では指定のサブディレクトリ名は転送時に保持されます
-
https://example.com/girls-side-analysis→http://girls-side-analysis-nextjs:3000/girls-side-analysis -
https://example.com/girls-side-analysis/葉月珪→http://girls-side-analysis-nextjs:3000/girls-side-analysis/葉月珪
結論から言うと、この設定の方が Next.js との相性が良いと考えます
もう一つの選択肢はこれです
location /girls-side-analysis {
proxy_pass http://girls-side-analysis-nextjs:3000/; # 末尾の "/" あり
}
末尾の "/" を trailing slash と呼び、Trailing slash 有りだと指定のサブディレクトリ名が転送時に省かれます
-
https://example.com/girls-side-analysis→http://girls-side-analysis-nextjs:3000 -
https://example.com/girls-side-analysis/葉月珪→http://girls-side-analysis-nextjs:3000/葉月珪
一般的にはこちらの構成が好まれる印象が有りますが、以下の理由から Next.js との相性が悪いです
Next.js の basePath 設定
上記の Nginx リバースプロキシ設定を行ったならば、 http://girls-side-analysis:3000/girls-side-analysis へのアクセス時に app/page.tsx を表示したいです
next.config.ts ファイルで設定を行います
import type { NextConfig } from 'next';
const nextConfig = {
basePath: '/girls-side-analysis',
//assetPrefix: '/girls-side-analysis',
//publicRuntimeConfig: {
// basePath: '/girls-side-analysis',
//},
//webpack: (config) => ({
// ...config,
// optimization: { minimize: false }
//}),
output: 'standalone',
serverExternalPackages: ['mysql2'],
allowedDevOrigins: ['local.test'],
} satisfies NextConfig;
export default nextConfig;
これで Nginx + Next.js の設定が完了で、サブディレクトリ下で Next.js が動くようになります
Auth.js の設定
Auth.js は近年新しいバージョン v5 が出たためか、ドキュメントが整備されていません
先に必要/不要な設定を列挙しておくと、
- 環境変数
NEXTAUTH_URLまたはAUTH_URLは指定しなくても大丈夫です -
src/auth.tsで basePath を設定します、trustHost: trueも設定します -
src/middleware.tsでsrc/auth.tsの項目が反映された middleware を設定します -
app/api/auth/[...nextauth]/route.tsに設定する認証用エントリポイントで、アクセスされたパスに basePath を付け足します
(恐らく Auth.js の basePath 解析が一部不十分なのでは...?) - 必要ならば
signIn,signOut関数を呼ぶ際にリダイレクト先を指定します。もしくはsrc/auth.tsのpages項目を設定します。 -
src/middleware.tsはキャッシュされる場合もあるかもしれないので、念のため開発環境をリロード(Next.js コンテナをリスタートし、再度next dev)します
もしかしたら、アップデートによって上記の手順が一部不要になったり、実はもっと自然な設定方法がある...かもしれません
(その際には是非コメント等で教えて下さい)
src/auth.ts
こんな感じになります
import NextAuth, { DefaultSession, Profile, Session } from 'next-auth';
import type { JWT } from 'next-auth/jwt';
import Twitter from 'next-auth/providers/twitter';
declare module 'next-auth' {
interface Profile {
data: {
name: string;
profile_image_url: string;
id: string;
username: string;
}
}
interface Session {
user: {
username: string;
} & DefaultSession['user']
}
}
declare module 'next-auth/jwt' {
interface JWT {
id: string;
username: string;
}
}
export const { auth, handlers, signIn, signOut } = NextAuth({
trustHost: true,
session: { strategy: 'jwt' },
providers: [Twitter],
debug: process.env.NODE_ENV !== 'production',
basePath: '/girls-side-analysis/api/auth',
pages: {
error: '/girls-side-analysis/profile',
signIn: '/girls-side-analysis/profile',
signOut: '/girls-side-analysis/profile',
},
callbacks: {
async jwt({ token, account, profile }) {
if (account && profile) {
token.id = profile.data.id;
token.username = profile.data.username;
}
return token;
},
async session({ session, token }) {
if (token?.id) {
session.user.id = token.id;
session.user.username = token.username;
}
return session;
},
}
});
ポイントは以下になります
-
debug: trueを指定するとその名の通りデバッグに有用なだけでなく、OAuth プロバイダからの様々な情報を受け取るためにjwt関数やsession関数をどう実装するべきかも判断しやすくなります(コンソールに情報が表示されます) -
trustHost: trueは docker (compose) 環境で用いるために必要です - basePath には Auth.js 用の認証用URLを指定します、
/grils-side-analysis/api/authを指定しています -
callbacks: { async jwt(), async session() }には、OAuth プロバイダからの情報を JWT トークンに格納したり、JWT トークンから session を復元する方法を指定します
OAuth Provider (Github, X(Twitter), Google) によって異なる処理が必要な場合が有ります
src/middleware.ts
どのページを middleware による認証の対象とするか選択します
export { auth as middleware } from '@/auth';
export const config = {
matcher: ['/profile'],
};
今回は /profile ページで
- 未認証の場合はサインイン用コンポーネント
- 認証済みの場合はユーザ毎のコンテンツ+サインアウト用コンポーネント
を切り替えて表示しますので、/profileページのみが対象になっています。
`src/app/api/auth/[...nextauth]/route.ts
特に引っかかったポイントです、この辺に該当する Issue があります Auth.js の basePath 設定と、上記URLの解析([...nextauth] 部分のマッチング処理など)は現状噛み合っておらず、OAuth Provider 側でエラーが表示されたり、サインイン後に意図しないページにリダイレクトされたりします
対策として /api/auth/[...nextauth] へのアクセス時に、「アクセスされたのは /girls-side-analysis/api/auth/[...nextauth] ですよ」と嘘をつきます、こんな感じです
import { handlers } from '@/auth';
//export const { GET, POST } = handlers;
//export const runtime = 'edge';
import { NextRequest } from 'next/server';
const toForwardedRequest = (req: NextRequest): NextRequest => {
const forwardedHost = req.headers.get('x-forwarded-host');
const forwardedProto = req.headers.get('x-forwarded-proto');
if (forwardedHost && forwardedProto) {
const forwardedUrl =
`${forwardedProto}://${forwardedHost}/girls-side-analysis${req.nextUrl.pathname}?${req.nextUrl.searchParams.toString()}`;
const newReq = new NextRequest(forwardedUrl, {
headers: req.headers,
method: req.method,
body: req.body,
});
return newReq;
} else {
return req;
}
}
export const GET: (req: NextRequest) => Promise<Response> =
(req) => handlers.GET(toForwardedRequest(req));
export const POST: (req: NextRequest) => Promise<Response> =
(req) => handlers.POST(toForwardedRequest(req));
signIn(), signOut() 関数のリダイレクト指定
src/auth.ts の pages オプションと重複するかもしれませんが、Auth.js の signIn(), signOut() 関数はオプション引数でリダイレクト先を指定できます
規定値は直前にいたページになるので、それでよければ指定の必要はありません
signIn('twitter', { redirectTo: 'https://example.com/girls-side-analysis/profile' });
signOut({ redirectTo: 'https://example.com/girls-side-analysis/profile' });
docker-compose.yml
この様な docker-compose ファイルで開発環境を起動しています。
services:
girls-side-analysis-nextjs:
build:
dockerfile: ./Dockerfile.nextjs
container_name: girls-side-analysis-nextjs-dev
environment:
TZ: 'Asia/Tokyo'
volumes:
- .env.development:/app/.env
- .:/app
tmpfs:
- /app/.next
database:
build:
dockerfile: ./Dockerfile.mysql
container_name: girls-side-analysis-database
env_file:
- .env.development
# 注. テスト用、データは永続しない
tmpfs:
- /var/lib/mysql
environment:
TZ: 'Asia/Tokyo'
LANG: 'ja_JP.UTF-8'
cap_add:
- SYS_NICE
healthcheck:
test: mysql -u $$MYSQL_USER -p$$MYSQL_PASSWORD $$MYSQL_DATABASE -e "select 1;"
interval: 5s
timeout: 20s
retries: 3
start_period: 5s
database-preparation:
image: node:20
#build:
# dockerfile: ./Dockerfile.database-preparation
container_name: girls-side-analysis-database-preparation
env_file:
- .env.development
environment:
NODE_ENV: 'development'
volumes:
- ./:/app:ro
working_dir: /app
command: >
bash -c '
npm install -g pnpm \
&& pnpm drizzle-kit push \
--dialect=mysql \
--schema=./src/db/schema.ts \
--host=$$DB_HOST \
--user=$$MYSQL_USER \
--password=$$MYSQL_PASSWORD \
--database=$$MYSQL_DATABASE; \
pnpm tsx addTestData.ts
'
depends_on:
database:
condition: service_healthy
webserver:
image: nginx
container_name: girls-side-analysis-webserver
ports:
- 80:80
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
networks:
default:
name: girls-side-analysis-network
さいごに
Vercel ではサブドメインを用いて複数の Next.js アプリケーションを管理しますから、わざわざ上記の構成で動かすもの好きはいない......のかもしれません
何かお役に立てば幸いです!
Discussion