99aeda5909
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)
207 lines
5.6 KiB
JavaScript
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();
|
|
};
|
|
}
|