昨日,思わぬところでハマってしまったので,共有いたします.
状況
aws_serverless_express
越しに,aws lambda上で,express
を動かしたときに,Set-Cookie
ヘッダが発行されないという問題に数時間悩まされました.
https://github.com/microsoft/TypeScript-Node-Starter/ を参考に,ほぼ同様の構成で,
- express
- express-session
- passport
- providerは,microsoft
でログイン機能をつけてみたのですが,oauth callbackが無事に呼ばれて,User認証できても,いっこうにブラウザにsession cookieが発行されず,ログインできないのでした.
結論としては,cookieのoptionに,secure: true
をつけていたが,aws lambdaで実行しているexpressがlistenしているconnnectionは,httpsでないため,cookieが発行されない,ということでした.
どういうことか
一般に,AWS上で,SAMアプリケーションを作ると,Client -> API Gateway -> Lambda
という経路をたどることになります.このうち,Client -> API Gateway
についてはもちろん,API Gateway -> Lambda
もHTTPSでの通信になっています.このため,CookieのSecure属性をtrue
にしても,問題なく動作すると考えていました.
ところが,aws_serverless_express
を使っているというところに思わぬ見落としがありました.
そこで,aws_serverless_express
がどういう処理をやっているのか覗いてみましょう.まず,APIGateway Lambda Integrationによって,http通信の情報をAPIGatewayEvent
というオブジェクトに格納して,Lambda関数が実行されます.
aws_serverless_express
は,このAPIGatewayEvent
をその名もapApiGatewayEventToHttpRequestという関数に渡して,Node.jsの,http.request
のoption引数に変換します.
そして,同一のプロセス内にexpress serverに起動して(!),そこに向けてリクエストを投げているのです.
つまり,lamdbaまでの経路はすべてhttpsであっても,最後の最後,lambdaの実行プロセスからexpressのhandlerが実行される部分で,httpになっていたのです.
通常のproxyであれば,X-Forward-For
に,自分自身のIPを追加したりしますが,aws_serverless_express
は厳密には,proxyではないため,X-Forward-For
を変更することなく,ヘッダ情報をほぼそのまま横流ししているため,HTTP Request/Respnseをデバッグしているだけでは見つけづらかった問題でした.
対応
原因がわかったので,対策に移りましょう.では,どうすればSecure属性を保ったまま,Cookieを発行できるかというと,
- express全体の設定として,
trust proxy
を設定する
つまり,
const app = express();
app.set("trust proxy", true);
または,
- express-sessionのcookieに関する設定で,trustProxyを設定する
const app = express();
app.use(
session({
// 本稿に関係のないオプションは非表示
cookie: {
proxy:true,
secure: true,
maxAge: 24 * 60 * 60 * 1000,
},
})
);
をします.aws_serverless_express
を使うのなら,1を選択するのがおすすめですが,影響範囲が読めない場合2でもいいでしょう.
trustProxy
をtrue
にすると,どういう挙動になるか,expressのreq.protocol
の実装を見てみましょう.
// https://github.com/expressjs/express/blob/4.17.1/lib/request.js#L307-L323
defineGetter(req, 'protocol', function protocol(){
// (筆者コメント: aws_serverless_express越しなので,ここは,'http')
var proto = this.connection.encrypted
? 'https'
: 'http';
var trust = this.app.get('trust proxy fn');
// (筆者コメント: trustProxyがFalseの場合,ここで'http'が返され,secureじゃないねとなる)
if (!trust(this.connection.remoteAddress, 0)) {
return proto;
}
// (筆者コメント: trustProxyがTrueの場合,'X-Forwarded-Proto'を読みに行ってくれて,結果'https'となる)
// Note: X-Forwarded-Proto is normally only ever a
// single value, but this is to be safe.
var header = this.get('X-Forwarded-Proto') || proto
var index = header.indexOf(',')
return index !== -1
? header.substring(0, index).trim()
: header.trim()
});
つまり,trustProxy
をtrue
にすると,今直接通信しているconnectionの状態ではなくて,HTTPのX-Forwarded-Proto
ヘッダを信頼し,その設定を元に,secureかどうか判断するようになるというわけです.
結果,aws_serverless_express
な環境でもSecureなcookieを発行することができました.
おしまい.