使用 apollo-studio 與 express-session 無法 set-cookie 問題

Apollo Server 3 後,將 graphql-playground 改為自家的 apollo-studio, 當你一開啟

http://localhost:{YOUR_DEV_PORT} 時,它會主動導頁到 https://studio.apollographql.com

一開始用的時候,覺得特別好用,apollo-studio 的 auto-complete 的功能比 graphql-playground 好用許多,但是做到登入的功能時遇到這個問題困擾我超久,明明就有給 set-cookie 的 header,但是怎麼樣都拿不到 cookie

問題

apollo-studio 為 https, cookie 裡面必須要有 SameSite=None; Secure 並且將 resonse header 加入以下兩個選項才能 set-cookie 成功

Access-Control-Allow-Origin: https://studio.apollographql.com
Access-Control-Allow-Credentials: true

如果 set-cookie 裡沒有 SameSite=None; Secure 即便 res 的 header 有 set-cookie 也會被 chrome 直接略過

我們可以用以下程式,將 SameSite=None; Secure 加入 cookie

res.cookie('testCookie', 'haha', { sameSite: 'none', secure: true })

但是 express-session 的 cookie 選項 secure 選項必須要是 https 開啟的狀態它才會將 secure 加到 cookie 裡,原碼裡面有特別寫 function 去驗證是否為 https 連線,如果secure 選項給 true 但卻非 https 連線,它就會直接不 setCookie

  • express-session 源碼
function issecure(req, trustProxy) {
// socket is https server
if (req.connection && req.connection.encrypted) {
return true;
}

// do not trust proxy
if (trustProxy === false) {
return false;
}

// no explicit trust; try req.secure from express
if (trustProxy !== true) {
return req.secure === true
}

// read the proto from x-forwarded-proto header
var header = req.headers['x-forwarded-proto'] || '';
var index = header.indexOf(',');
var proto = index !== -1
? header.substr(0, index).toLowerCase().trim()
: header.toLowerCase().trim()

return proto === 'https';
}
onHeaders(res, function(){
if (!req.session) {
debug('no session');
return;
}

if (!shouldSetCookie(req)) {
return;
}

// only send secure cookies via https
if (req.session.cookie.secure && !issecure(req, trustProxy)) {
debug('not secured');
return;
}

if (!touched) {
// touch session
req.session.touch()
touched = true
}

console.log(req.session.cookie.data)
// set cookie
setcookie(res, name, req.sessionID, secrets[0], req.session.cookie.data);
});

解法一

使用 ApolloServerPluginLandingPageGraphQLPlayground plugin,用舊版 apollo-server 內建 graphql 提供的 playground,跑在 local 端就不會遇到以上問題

import { ApolloServer } from 'apollo-server-express'
import { ApolloServerPluginLandingPageGraphQLPlayground } from 'apollo-server-core'
import express from 'express'

const app = express()

app.use(
session({
name: 'qid',
store: new RedisStore({ client: redisClient, disableTouch: true }),
cookie: {
maxAge: 1000 * 60 * 60 * 24 * 5, // 5 days
httpOnly: true,
sameSite: 'lax',
secure: __prod__
},
secret: 'wefjlewf',
resave: false,
saveUninitialized: false // do not create empty session by default
})
)

const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [PostResolver, UserResolver]
}),
context: ({ req, res }) => ({ em: orm.em, req, res }),
plugins: [
ApolloServerPluginLandingPageGraphQLPlayground()
]
})

解法二

用 patch-package 新增 patch, 將判斷是否為 https 連線的地方新增是否為 production 環境

const isProduction = process.env.NODE_ENV === 'production'
if ( req.session.cookie.secure && isProduction && !issecure(req, trustProxy)) {
debug('not secured');
return;
}

但需注意,若要部署到的 heroku 上面的話要新增設定,請參考文件

GitHub - ds300/patch-package: Fix broken node modules instantly 🏃🏽‍♀️💨

你真的懂 Node.js 裡 Libuv 如何使用 Thread Pool 嗎? Express 筆記
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×