リモートワーカーになって迎えた最初の試練はリモートならではの問題ではなく、Nginxでした。 NginxはWebサーバーなのでそこまで複雑な設定は必要がないと思っていました。 Railsの頃はPassengerを使えばよかったし、Node.jsをメインに使うようになってからもネット上のスニペットを貼り付けておしまい。

CORSというものは私の認識ではもともとそこまで厄介なものではありませんでした。 Nginxでreverse proxyをすればそもそも発生しませんし、なにか起こったとしてもcorsミドルウェアを使えばいい。 現在私が担当しているOAuth2サーバーもそのような認識でした。

しかし、OAuth2サーバーでは外部のサイトと連携する必要があるので当然 認可したあとに発行するトークンを使ってアクセスしようとするとたちまちうまくいかなくなりました。 当然それまで使っていたcorsも適用してみましたがうまくいきません。 これまで浅い知識で対処していたCORSについてきちんと向き合う必要が出てきました。

corsについて

まずはミドルウェア側から見てみましょう。 READMEのEnable CORS for a Single Routeをもとにしたコードです。

var express = require('express');
var cors = require('cors');
var app = express();

app.get('/', cors(), function (req, res, next) {
  res.send('hello world');
});

この実装でcurlコマンドでHTTPヘッダを調べてみると:

HTTP/1.1 200 OK
X-Powered-By: Express
Access-Control-Allow-Origin: *
Content-Type: text/html; charset=utf-8
Content-Length: 11
ETag: W/"b-Kq5sNclPz7QV2+lfQIuc6R7oRu0"
Date: Sat, 10 Oct 2020 07:34:54 GMT
Connection: keep-alive

このようにAccess-Control-Allow-Originが付与されます。 この時点で私が勘違いしていたのは例えばAccess-Control-Allow-Methodsという許可するメソッドを明示する諸々のヘッダは付与されないということでした。 これまでは何も考えずcors()とすればすべてうまくいっていたはずが、うまくいかずに苦しむことになります。

Nginxについて

検証のためにDockerコンテナでNginxで以下のようなコードを用意しました。

server {
    listen       80;
    listen  [::]:80;
    server_name  _;
    charset utf-8;
    location / {
        proxy_pass         http://demo:3000;
        proxy_redirect     off;
        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-Host $server_name;
    }
}

http://demo:3000がExpress側のコンテナにつながっているので、この状態で再びcurlしてみました。

HTTP/1.1 200 OK
Server: nginx/1.19.2
Date: Sat, 10 Oct 2020 08:37:10 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 11
Connection: keep-alive
X-Powered-By: Express
Access-Control-Allow-Origin: *
ETag: W/"b-Kq5sNclPz7QV2+lfQIuc6R7oRu0"

すると、このように何故かAccess-Control-Allow-Originがbypassされてしまいました。 本来はここでこのヘッダが出てこなくなっていたので、そのヘッダを表示するためにproxy_set_headerを使いましょうという理解でした。

いずれにしても私のサーバーではcorsの設定とproxy_set_headerがうまく反映されなかったのでcorsを削除し、Nginx側でadd_headerを使いました。 add_headerをするとAccess-Control-Allow-Originが二重に表示されるエラーも出ましたが、Nginx側でヘッダを管理してしまえば少なくともExpressで設定したのに表示されずに悩むこともなくなりました。

とはいえ理想はExpress側で管理できるようにしたいので、あくまでも暫定的な修正ですけれども。

CORSのヘッダについて

Access-Control-Allow-Originというヘッダに*(ワイルドカード)を指定すればひとまず問題解決かというと、そんなことはありませんでした。 というのも、今になって気づいたのはCookieを使用していたからかもしれません。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Credentialed_requests_and_wildcards

資格情報を含むリクエストに対するレスポンスの時、サーバーは Access-Control-Allow-Origin ヘッダーで “*” ワイルドカードではなくオリジンを指定しなければなりません

今回うまくいかなかった原因としては、この一文を見落としていたのも大きいです。 FireFoxが原因で動かないとかではなく、Chromiumブラウザでも同様のエラーが起きていました。 最終的には以下の4つのヘッダを個別に指定して解決しています。

add_header Access-Control-Allow-Origin      *;
add_header Access-Control-Allow-Credentials true;
add_header Access-Control-Allow-Methods     "POST, GET, OPTIONS";
add_header Access-Control-Allow-Headers     "Authorization";

これらのヘッダに関する解説はMDNの方を参照してもらうとして、単純にcors()というオプションだけではうまくいかなかったこと。 curlを使ってヘッダの情報が正しく表示されているかデバッグすること。 解決方法がわかった今だからこそ冷静にドキュメントを読む余裕も出ているのですが、一度どうやっても動かないと思い込んでしまうと同じ情報でも見えるものがずいぶん異なるものです。

まだ理解しきれていない部分もあるので分かり次第更新する予定ですが、Nginxを軽んじて痛い目にあうのが今回の教訓でした。