Ghost/ghost/members-api/index.js
Fabien O'Carroll 99aeda5909 Removed ssoOriginCheck from signout endpoint (#10277)
no-issue

the ssoOriginCheck exists to ensure that we only allow signin/signup to
be called from the specified auth page, this is a very minor security
feature in that it forces signins to go via the page you've designated.
signout however does not need this protection as the call to signout
completely bypasses any UI (this is the same for the call to /token)
2019-05-07 17:35:17 +02:00

207 lines
5.6 KiB
JavaScript

const jose = require('node-jose');
const {Router, static} = require('express');
const body = require('body-parser');
const jwt = require('jsonwebtoken');
const cookies = require('./cookies');
module.exports = function MembersApi({
config: {
issuer,
privateKey,
publicKey,
sessionSecret,
ssoOrigin
},
validateAudience,
createMember,
validateMember,
updateMember,
getMember,
sendEmail
}) {
const keyStore = jose.JWK.createKeyStore();
const keyStoreReady = keyStore.add(privateKey, 'pem');
const router = Router();
const apiRouter = Router();
apiRouter.use(body.json());
apiRouter.use(function waitForKeyStore(req, res, next) {
keyStoreReady.then((jwk) => {
req.jwk = jwk;
next();
});
});
const {getCookie, setCookie, removeCookie} = cookies(sessionSecret);
apiRouter.post('/token', getData('audience'), (req, res) => {
const {signedin} = getCookie(req);
if (!signedin) {
res.writeHead(401, {
'Set-Cookie': removeCookie()
});
return res.end();
}
const {audience, origin} = req.data;
validateAudience({audience, origin, id: signedin}).then(() => {
const token = jwt.sign({
sub: signedin,
kid: req.jwk.kid
}, privateKey, {
algorithm: 'RS512',
audience,
issuer
});
return res.end(token);
}).catch(handleError(403, res));
});
function ssoOriginCheck(req, res, next) {
if (!req.data.origin || req.data.origin !== ssoOrigin) {
res.writeHead(403);
return res.end();
}
next();
}
apiRouter.post('/request-password-reset', getData('email'), ssoOriginCheck, (req, res) => {
const {email} = req.data;
const memberPromise = getMember({email});
memberPromise.catch(() => {
res.writeHead(200);
res.end();
});
memberPromise.then((member) => {
const token = jwt.sign({
sub: member.id,
kid: req.jwk.kid
}, privateKey, {
algorithm: 'RS512',
issuer
});
return sendEmail(member, {token});
}).then(() => {
res.writeHead(200);
res.end();
}).catch(handleError(500, res));
});
apiRouter.post('/reset-password', getData('token', 'password'), ssoOriginCheck, (req, res) => {
const {token, password} = req.data;
try {
jwt.verify(token, publicKey, {
algorithm: 'RS512',
issuer
});
} catch (err) {
res.writeHead(401);
return res.end();
}
const id = jwt.decode(token).sub;
updateMember({id}, {password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
apiRouter.post('/signup', getData('name', 'email', 'password'), ssoOriginCheck, (req, res) => {
const {name, email, password} = req.data;
// @TODO this should attempt to reset password before creating member
createMember({name, email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(400, res));
});
apiRouter.post('/signin', getData('email', 'password'), ssoOriginCheck, (req, res) => {
const {email, password} = req.data;
validateMember({email, password}).then((member) => {
res.writeHead(200, {
'Set-Cookie': setCookie(member)
});
res.end();
}).catch(handleError(401, res));
});
apiRouter.post('/signout', getData(), (req, res) => {
res.writeHead(200, {
'Set-Cookie': removeCookie()
});
res.end();
});
const staticRouter = Router();
staticRouter.use('/static', static(require('path').join(__dirname, './static/auth/dist')));
staticRouter.use('/gateway', static(require('path').join(__dirname, './static/gateway')));
staticRouter.get('/*', (req, res) => {
res.sendFile(require('path').join(__dirname, './static/auth/dist/index.html'));
});
router.use('/api', apiRouter);
router.use('/static', staticRouter);
router.get('/.well-known/jwks.json', (req, res) => {
keyStoreReady.then(() => {
res.json(keyStore.toJSON());
});
});
function httpHandler(req, res, next) {
return router.handle(req, res, next);
}
httpHandler.staticRouter = staticRouter;
httpHandler.apiRouter = apiRouter;
httpHandler.keyStore = keyStore;
return httpHandler;
};
function getData(...props) {
return function (req, res, next) {
if (!req.body) {
res.writeHead(400);
return res.end();
}
const data = props.concat('origin').reduce((data, prop) => {
if (!data || !req.body[prop]) {
return null;
}
return Object.assign(data, {
[prop]: req.body[prop]
});
}, {});
if (!data) {
res.writeHead(400);
return res.end(`Expected {${props.join(', ')}}`);
}
req.data = data || {};
next();
};
}
function handleError(status, res) {
return function () {
res.writeHead(status);
res.end();
};
}