Nginxリバースプロキシ環境下におけるOAuth2 Clientの問題解決

問題の概要

OAuth2 Clientを統合したサービスにおいて、ローカル環境やアプリケーションに直接アクセスする限り問題ありませんでしたが、本番環境でNginxを経由した転送を行うと、OAuthログインが常に失敗するようになりました。

具体的には、認証が必要なAPIエンドポイント https://blog.95id.com:4005/user_attr にアクセスすると、認証サーバー(例: GitHub)へのリダイレクトが期待されますが、実際には http://blog.95id.com/login にリダイレクトされてしまいます。

原因の特定と段階的な解決

複雑なNginx構成(例: 2層のNginx)が原因で問題の切り分けが難しかったため、単純なシナリオから段階的に検証を行いました。

シナリオ1: Nginx (ポート80) が OAuth Client (ポート8082) をプロキシ

最も単純な構成です。この場合、リダイレクトは正常に動作します。

アプリケーション設定 (application.properties):

server.port=8082

Nginx設定:

server {
    listen 80;
    server_name blog.95id.com;
    port_in_redirect off;
    location / {
        add_header Cache-Control no-store;
        proxy_pass http://127.0.0.1:8082;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

シナリオ2: Nginx (非80ポート: 4005) が OAuth Client (ポート8082) をプロキシ

ポート4005でリクエストを受け付け、バックエンドに転送します。この場合、リダイレクト先からポート情報が失われる問題が発生します。

問題の動作:

  • リクエスト: http://blog.95id.com:4005/user_attr
  • リダイレクト先: http://blog.95id.com/login (ポート4005が欠落)

原因: Spring SecurityのOAuth2 Clientライブラリが、プロキシされたリクエストから正しいポート番号を取得できないためです。

解決策: Nginx設定で Host ヘッダーにポート番号を明示的に含めます。

proxy_set_header Host $host:$server_port;

この設定により、アプリケーションはリクエストURLからポート4005を正しく認識できるようになります。

シナリオ3: Nginx (HTTPS ポート443) が OAuth Client (ポート8082) をプロキシ

HTTPS経由でアクセスすると、リダイレクト先のプロトコルがHTTPに変わってしまう問題が発生します。

問題の動作:

  • リクエスト: https://blog.95id.com/user_attr
  • リダイレクト先: http://blog.95id.com/login (HTTPSがHTTPに)

解決策: Nginxからアプリケーションに、元のリクエストプロトコル (HTTPS) を伝える必要があります。

Nginx設定:

proxy_set_header X-Forwarded-Proto $scheme;

アプリケーション設定 (application.properties):

server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.protocol-header=X-Forwarded-Proto
server.use-forward-headers=true

シナリオ4: Nginx (HTTPS 非443ポート: 4005) が OAuth Client (ポート8082) をプロキシ

HTTPSと非標準ポートの両方に対応する必要がある場合です。このシナリオでは、ポート情報が再び失われる問題が発生します。

問題の動作:

  • リクエスト: https://blog.95id.com:4005/user_attr
  • リダイレクト先: https://blog.95id.com/login (ポート4005が欠落)

原因: Spring Bootのデフォルト設定では、X-Forwarded-Port ヘッダーを参照しないためです。

解決策: X-Forwarded-Port ヘッダーを使用してポート情報を明示的に渡します。

Nginx設定:

proxy_set_header X-Forwarded-Port $server_port;

アプリケーション設定 (application.properties):

server.tomcat.port-header=X-Forwarded-Port

シナリオ5: 2層のNginx リバースプロキシ

複数層のNginxを経由する場合、プロトコルとポート情報が適切に伝搬されない問題が発生します。

アーキテクチャ: クライアント → Nginx1 (ポート4005, SSL終端) → Nginx2 (ポート4005) → OAuth Client (ポート8082)

問題: Nginx1がHTTPSをHTTPに変換してNginx2に転送するため、Nginx2は元のプロトコルがHTTPSであることを認識できません。

解決策: プロトコル情報をNginx1からNginx2へ伝搬する必要があります。

Nginx1 (最外部) の設定:

proxy_set_header 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 X-Forwarded-Port $server_port;

Nginx2 (内部) の設定: 受信した X-Forwarded-Proto ヘッダーをそのまま転送する必要があります。以下の2つの方法があります。

方法1: プロトコルを強制的にHTTPSに固定する (全てのトラフィックがHTTPSである場合)

proxy_set_header X-Forwarded-Proto https;

方法2: map ディレクティブを使用して動的に設定する (HTTPとHTTPSが混在する可能性がある場合)

http コンテキスト:

map $http_x_forwarded_proto $thescheme {
    default $http_x_forwarded_proto;
    ''      $scheme;
}

server または location コンテキスト:

proxy_set_header X-Forwarded-Proto $thescheme;

最終的な解決策とベストプラクティス

上記のすべてのシナリオを考慮した、汎用的な設定を以下に示します。この設定は、標準ポート、HTTPS、非標準ポート、多層プロキシのいずれの環境でも機能します。

1. アプリケーション設定 (application.properties):

server.tomcat.remote-ip-header=x-forwarded-for
server.tomcat.protocol-header=X-Forwarded-Proto
server.use-forward-headers=true
server.tomcat.port-header=X-Forwarded-Port

2. Nginx設定:

http コンテキスト:

map $http_x_forwarded_proto $thescheme {
    default $http_x_forwarded_proto;
    ''      $scheme;
}

各 server コンテキスト (プロキシ設定):

server {
    listen 4005; # 例: リスニングポート
    server_name example.com;
    # ... SSL設定など ...

    location / {
        proxy_pass http://127.0.0.1:8082; # バックエンドアプリケーション

        proxy_set_header 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 $thescheme;
        proxy_set_header X-Forwarded-Port $server_port;
    }
}

外部ロードバランサー/CDNを使用する場合: もしSSL終端が外部で行われており、Nginxが常にHTTPリクエストを受信する場合は、$scheme 変数が常に http になります。この場合、プロトコルを強制的にHTTPSに設定します。

proxy_set_header X-Forwarded-Proto "https";

Spring Boot以外のTomcatを使用する場合: 内蔵Tomcatを使用しない場合は、server.xmlRemoteIpValve を追加します。

<Valve className="org.apache.catalina.valves.RemoteIpValve"
       remoteIpHeader="X-Forwarded-For"
       protocolHeader="X-Forwarded-Proto"
       portHeader="X-Forwarded-Port"/>

補足: 関連するSpring Boot / Nginx設定の解説

Spring Boot プロパティ

  • server.tomcat.port-header: ポート情報を上書きするために使用するHTTPヘッダー名を指定します (例: X-Forwarded-Port)。
  • server.tomcat.protocol-header: プロトコル情報を含むHTTPヘッダー名を指定します (例: X-Forwarded-Proto)。
  • server.tomcat.protocol-header-https-value: プロトコルヘッダーの値がHTTPSであることを示す文字列を指定します (デフォルト: https)。
  • server.tomcat.remote-ip-header: クライアントのリモートIPアドレスを含むHTTPヘッダー名を指定します (例: X-Forwarded-For)。
  • server.use-forward-headers=true: この設定により、Tomcatは HttpServletRequest オブジェクトから直接取得する代わりに、上記で指定したヘッダーからプロトコル、ポート、リモートIP情報を取得するようになります。

Nginx ディレクティブ

  • proxy_set_header: バックエンドサーバーに送信されるリクエストヘッダーを設定または上書きします。
  • $scheme: リクエストのプロトコル (http または https) を表す変数です。
  • $server_port: サーバーがリクエストを受信したポート番号を表す変数です。
  • $host: リクエストの Host ヘッダーの値を表す変数です。
  • $http_x_forwarded_proto: 受信したリクエストの X-Forwarded-Proto ヘッダーの値を表す変数です。
  • map: 変数の値を他の値にマッピングするためのディレクティブです。ここでは、受信した X-Forwarded-Proto ヘッダーが存在する場合はその値を、存在しない場合は $scheme 変数の値を使用するために使用しています。

6月9日 21:59 投稿