🌐

リバースプロキシ下で 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)に転送する設定です

nginx.conf

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-analysishttp://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-analysishttp://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 ファイルで設定を行います

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.tssrc/auth.ts の項目が反映された middleware を設定します
  • app/api/auth/[...nextauth]/route.ts に設定する認証用エントリポイントで、アクセスされたパスに basePath を付け足します
    (恐らく Auth.js の basePath 解析が一部不十分なのでは...?)
  • 必要ならば signIn, signOut 関数を呼ぶ際にリダイレクト先を指定します。もしくは src/auth.tspages 項目を設定します。
  • src/middleware.ts はキャッシュされる場合もあるかもしれないので、念のため開発環境をリロード(Next.js コンテナをリスタートし、再度 next dev)します

もしかしたら、アップデートによって上記の手順が一部不要になったり、実はもっと自然な設定方法がある...かもしれません
(その際には是非コメント等で教えて下さい)

src/auth.ts

こんな感じになります

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 による認証の対象とするか選択します

src/middleware.ts
export { auth as middleware } from '@/auth';

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

今回は /profile ページで

  • 未認証の場合はサインイン用コンポーネント
  • 認証済みの場合はユーザ毎のコンテンツ+サインアウト用コンポーネント
    を切り替えて表示しますので、/profile ページのみが対象になっています。

`src/app/api/auth/[...nextauth]/route.ts

特に引っかかったポイントです、この辺に該当する Issue があります
https://github.com/nextauthjs/next-auth/issues/10928#issuecomment-2144241314
Auth.js の basePath 設定と、上記URLの解析([...nextauth] 部分のマッチング処理など)は現状噛み合っておらず、OAuth Provider 側でエラーが表示されたり、サインイン後に意図しないページにリダイレクトされたりします

対策として /api/auth/[...nextauth] へのアクセス時に、「アクセスされたのは /girls-side-analysis/api/auth/[...nextauth] ですよ」と嘘をつきます、こんな感じです

src/app/api/auth/[...nextauth]/route.ts
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.tspages オプションと重複するかもしれませんが、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 ファイルで開発環境を起動しています。

docker-compose.yml
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 アプリケーションを管理しますから、わざわざ上記の構成で動かすもの好きはいない......のかもしれません
何かお役に立てば幸いです!

GitHubで編集を提案

Discussion