diff --git a/ghost/members-ssr/index.js b/ghost/members-ssr/index.js index e69de29bb2..2f49776527 100644 --- a/ghost/members-ssr/index.js +++ b/ghost/members-ssr/index.js @@ -0,0 +1,106 @@ +const concat = require('concat-stream'); +const Cookies = require('cookies'); +const jwt = require('jsonwebtoken'); +const ignition = require('ghost-ignition'); + +const { + UnauthorizedError, + BadRequestError +} = ignition.errors; + +const EMPTY = {}; +const SIX_MONTHS_MS = 1000 * 60 * 60 * 24 * 184; + +const wrapFn = (fn, cookieConfig) => (req, res) => { + return new Promise((resolve, reject) => { + const cookies = new Cookies(req, res, cookieConfig); + req.on('error', reject); + req.pipe(concat(function (buff) { + const body = buff.toString(); + resolve(fn(req, res, {body, cookies})); + })); + }); +}; + +module.exports = function create(options = EMPTY) { + if (options === EMPTY) { + throw new Error('Must pass options'); + } + + const { + cookieMaxAge = SIX_MONTHS_MS, + cookieSecure = true, + cookieName = 'members-ssr', + cookiePath = '/', + cookieKeys, + membersApi + } = options; + + if (!membersApi) { + throw new Error('Missing option membersApi'); + } + + if (!cookieKeys) { + throw new Error('Missing option cookieKeys'); + } + + const audience = ['members-ssr']; + + const cookieConfig = { + keys: [].concat(cookieKeys), + secure: cookieSecure + }; + + const verifyJwt = token => membersApi.getPublicConfig().then(({publicKey, issuer}) => { + return new Promise((resolve, reject) => { + jwt.verify(token, publicKey, { + algorithms: ['RS512'], + issuer, + audience + }, (err, claims) => { + if (err) { + reject(new UnauthorizedError({err})); + } + resolve(claims); + }); + }); + }); + + const exchangeTokenForSession = wrapFn((req, res, {body, cookies}) => { + const token = body; + if (!body || typeof body !== 'string') { + throw new BadRequestError({ + message: 'Expected body containing JWT' + }); + } + + return verifyJwt(token).then(() => { + cookies.set(cookieName, token, { + signed: true, + httpOnly: true, + sameSite: 'lax', + maxAge: cookieMaxAge, + path: cookiePath + }); + }); + }, cookieConfig); + + const getMemberDataFromSession = wrapFn((req, res, {cookies}) => { + const token = cookies.get(cookieName, { + signed: true + }); + if (!token) { + throw new BadRequestError({ + message: `Cookie ${cookieName} not found` + }); + } + return verifyJwt(token).then((claims) => { + return membersApi.getMember(claims.sub, token); + }); + }, cookieConfig); + + return { + exchangeTokenForSession, + getMemberDataFromSession + }; +};