mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-03 23:00:14 -05:00
parent
7ef520c2ea
commit
5ffdfe9875
4 changed files with 154 additions and 89 deletions
|
@ -1,9 +1,11 @@
|
||||||
const jose = require('node-jose');
|
|
||||||
const {Router, static} = require('express');
|
const {Router, static} = require('express');
|
||||||
const body = require('body-parser');
|
const body = require('body-parser');
|
||||||
const jwt = require('jsonwebtoken');
|
|
||||||
|
|
||||||
const cookies = require('./cookies');
|
const {getData, handleError} = require('./util');
|
||||||
|
|
||||||
|
const Cookies = require('./cookies');
|
||||||
|
const Tokens = require('./tokens');
|
||||||
|
const Users = require('./users');
|
||||||
|
|
||||||
module.exports = function MembersApi({
|
module.exports = function MembersApi({
|
||||||
config: {
|
config: {
|
||||||
|
@ -20,23 +22,28 @@ module.exports = function MembersApi({
|
||||||
getMember,
|
getMember,
|
||||||
sendEmail
|
sendEmail
|
||||||
}) {
|
}) {
|
||||||
const keyStore = jose.JWK.createKeyStore();
|
const {encodeToken, decodeToken, getPublicKeys} = Tokens({privateKey, publicKey, issuer});
|
||||||
const keyStoreReady = keyStore.add(privateKey, 'pem');
|
|
||||||
|
const users = Users({
|
||||||
|
createMember,
|
||||||
|
updateMember,
|
||||||
|
getMember,
|
||||||
|
validateMember,
|
||||||
|
sendEmail,
|
||||||
|
encodeToken,
|
||||||
|
decodeToken
|
||||||
|
});
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
const apiRouter = Router();
|
const apiRouter = Router();
|
||||||
|
|
||||||
apiRouter.use(body.json());
|
apiRouter.use(body.json());
|
||||||
apiRouter.use(function waitForKeyStore(req, res, next) {
|
|
||||||
keyStoreReady.then((jwk) => {
|
|
||||||
req.jwk = jwk;
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const {getCookie, setCookie, removeCookie} = cookies(sessionSecret);
|
/* session */
|
||||||
|
const {getCookie, setCookie, removeCookie} = Cookies(sessionSecret);
|
||||||
|
|
||||||
|
/* token */
|
||||||
apiRouter.post('/token', getData('audience'), (req, res) => {
|
apiRouter.post('/token', getData('audience'), (req, res) => {
|
||||||
const {signedin} = getCookie(req);
|
const {signedin} = getCookie(req);
|
||||||
if (!signedin) {
|
if (!signedin) {
|
||||||
|
@ -48,19 +55,16 @@ module.exports = function MembersApi({
|
||||||
|
|
||||||
const {audience, origin} = req.data;
|
const {audience, origin} = req.data;
|
||||||
|
|
||||||
validateAudience({audience, origin, id: signedin}).then(() => {
|
validateAudience({audience, origin, id: signedin})
|
||||||
const token = jwt.sign({
|
.then(() => encodeToken({
|
||||||
sub: signedin,
|
sub: signedin,
|
||||||
kid: req.jwk.kid
|
aud: audience
|
||||||
}, privateKey, {
|
}))
|
||||||
algorithm: 'RS512',
|
.then(token => res.end(token))
|
||||||
audience,
|
.catch(handleError(403, res));
|
||||||
issuer
|
|
||||||
});
|
|
||||||
return res.end(token);
|
|
||||||
}).catch(handleError(403, res));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* security */
|
||||||
function ssoOriginCheck(req, res, next) {
|
function ssoOriginCheck(req, res, next) {
|
||||||
if (!req.data.origin || req.data.origin !== ssoOrigin) {
|
if (!req.data.origin || req.data.origin !== ssoOrigin) {
|
||||||
res.writeHead(403);
|
res.writeHead(403);
|
||||||
|
@ -69,47 +73,21 @@ module.exports = function MembersApi({
|
||||||
next();
|
next();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* users, token, emails */
|
||||||
apiRouter.post('/request-password-reset', getData('email'), ssoOriginCheck, (req, res) => {
|
apiRouter.post('/request-password-reset', getData('email'), ssoOriginCheck, (req, res) => {
|
||||||
const {email} = req.data;
|
const {email} = req.data;
|
||||||
|
|
||||||
const memberPromise = getMember({email});
|
users.requestPasswordReset({email}).then(() => {
|
||||||
|
|
||||||
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.writeHead(200);
|
||||||
res.end();
|
res.end();
|
||||||
}).catch(handleError(500, res));
|
}).catch(handleError(500, res));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* users, token */
|
||||||
apiRouter.post('/reset-password', getData('token', 'password'), ssoOriginCheck, (req, res) => {
|
apiRouter.post('/reset-password', getData('token', 'password'), ssoOriginCheck, (req, res) => {
|
||||||
const {token, password} = req.data;
|
const {token, password} = req.data;
|
||||||
|
|
||||||
try {
|
users.resetPassword({token, password}).then((member) => {
|
||||||
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, {
|
res.writeHead(200, {
|
||||||
'Set-Cookie': setCookie(member)
|
'Set-Cookie': setCookie(member)
|
||||||
});
|
});
|
||||||
|
@ -117,11 +95,12 @@ module.exports = function MembersApi({
|
||||||
}).catch(handleError(401, res));
|
}).catch(handleError(401, res));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* users, email */
|
||||||
apiRouter.post('/signup', getData('name', 'email', 'password'), ssoOriginCheck, (req, res) => {
|
apiRouter.post('/signup', getData('name', 'email', 'password'), ssoOriginCheck, (req, res) => {
|
||||||
const {name, email, password} = req.data;
|
const {name, email, password} = req.data;
|
||||||
|
|
||||||
// @TODO this should attempt to reset password before creating member
|
// @TODO this should attempt to reset password before creating member
|
||||||
createMember({name, email, password}).then((member) => {
|
users.create({name, email, password}).then((member) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Set-Cookie': setCookie(member)
|
'Set-Cookie': setCookie(member)
|
||||||
});
|
});
|
||||||
|
@ -129,10 +108,11 @@ module.exports = function MembersApi({
|
||||||
}).catch(handleError(400, res));
|
}).catch(handleError(400, res));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* users, session */
|
||||||
apiRouter.post('/signin', getData('email', 'password'), ssoOriginCheck, (req, res) => {
|
apiRouter.post('/signin', getData('email', 'password'), ssoOriginCheck, (req, res) => {
|
||||||
const {email, password} = req.data;
|
const {email, password} = req.data;
|
||||||
|
|
||||||
validateMember({email, password}).then((member) => {
|
users.validate({email, password}).then((member) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Set-Cookie': setCookie(member)
|
'Set-Cookie': setCookie(member)
|
||||||
});
|
});
|
||||||
|
@ -140,6 +120,7 @@ module.exports = function MembersApi({
|
||||||
}).catch(handleError(401, res));
|
}).catch(handleError(401, res));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* session */
|
||||||
apiRouter.post('/signout', getData(), (req, res) => {
|
apiRouter.post('/signout', getData(), (req, res) => {
|
||||||
res.writeHead(200, {
|
res.writeHead(200, {
|
||||||
'Set-Cookie': removeCookie()
|
'Set-Cookie': removeCookie()
|
||||||
|
@ -147,6 +128,7 @@ module.exports = function MembersApi({
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* http */
|
||||||
const staticRouter = Router();
|
const staticRouter = Router();
|
||||||
staticRouter.use('/static', static(require('path').join(__dirname, './static/auth/dist')));
|
staticRouter.use('/static', static(require('path').join(__dirname, './static/auth/dist')));
|
||||||
staticRouter.use('/gateway', static(require('path').join(__dirname, './static/gateway')));
|
staticRouter.use('/gateway', static(require('path').join(__dirname, './static/gateway')));
|
||||||
|
@ -154,11 +136,13 @@ module.exports = function MembersApi({
|
||||||
res.sendFile(require('path').join(__dirname, './static/auth/dist/index.html'));
|
res.sendFile(require('path').join(__dirname, './static/auth/dist/index.html'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* http */
|
||||||
router.use('/api', apiRouter);
|
router.use('/api', apiRouter);
|
||||||
router.use('/static', staticRouter);
|
router.use('/static', staticRouter);
|
||||||
|
/* token */
|
||||||
router.get('/.well-known/jwks.json', (req, res) => {
|
router.get('/.well-known/jwks.json', (req, res) => {
|
||||||
keyStoreReady.then(() => {
|
getPublicKeys().then((jwks) => {
|
||||||
res.json(keyStore.toJSON());
|
res.json(jwks);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -168,39 +152,7 @@ module.exports = function MembersApi({
|
||||||
|
|
||||||
httpHandler.staticRouter = staticRouter;
|
httpHandler.staticRouter = staticRouter;
|
||||||
httpHandler.apiRouter = apiRouter;
|
httpHandler.apiRouter = apiRouter;
|
||||||
httpHandler.keyStore = keyStore;
|
httpHandler.memberUserObject = users;
|
||||||
|
|
||||||
return httpHandler;
|
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();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
42
ghost/members-api/tokens.js
Normal file
42
ghost/members-api/tokens.js
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
const jose = require('node-jose');
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
|
||||||
|
module.exports = function ({
|
||||||
|
privateKey,
|
||||||
|
publicKey,
|
||||||
|
issuer
|
||||||
|
}) {
|
||||||
|
const keyStore = jose.JWK.createKeyStore();
|
||||||
|
const keyStoreReady = keyStore.add(privateKey, 'pem');
|
||||||
|
|
||||||
|
function encodeToken({sub, aud = issuer}) {
|
||||||
|
return keyStoreReady.then(jwk => jwt.sign({
|
||||||
|
sub,
|
||||||
|
kid: jwk.kid
|
||||||
|
}, privateKey, {
|
||||||
|
algorithm: 'RS512',
|
||||||
|
audience: aud,
|
||||||
|
issuer
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function decodeToken(token) {
|
||||||
|
return keyStoreReady.then(jwk => jwt.verify(token, publicKey, {
|
||||||
|
algorithm: 'RS512',
|
||||||
|
kid: jwk.kid,
|
||||||
|
issuer
|
||||||
|
})).then(() => jwt.decode(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPublicKeys() {
|
||||||
|
return keyStoreReady.then(() => {
|
||||||
|
keyStore.toJSON();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
encodeToken,
|
||||||
|
decodeToken,
|
||||||
|
getPublicKeys
|
||||||
|
};
|
||||||
|
};
|
35
ghost/members-api/users.js
Normal file
35
ghost/members-api/users.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
module.exports = function ({
|
||||||
|
createMember,
|
||||||
|
updateMember,
|
||||||
|
getMember,
|
||||||
|
validateMember,
|
||||||
|
sendEmail,
|
||||||
|
encodeToken,
|
||||||
|
decodeToken
|
||||||
|
}) {
|
||||||
|
function requestPasswordReset({email}) {
|
||||||
|
return getMember({email}).then((member) => {
|
||||||
|
return encodeToken({
|
||||||
|
sub: member.id
|
||||||
|
}).then((token) => {
|
||||||
|
return sendEmail(member, {token});
|
||||||
|
});
|
||||||
|
}, (/*err*/) => {
|
||||||
|
// Ignore user not found err;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetPassword({token, password}) {
|
||||||
|
return decodeToken(token).then(({sub}) => {
|
||||||
|
return updateMember({id: sub}, {password});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
requestPasswordReset,
|
||||||
|
resetPassword,
|
||||||
|
create: createMember,
|
||||||
|
validate: validateMember,
|
||||||
|
get: getMember
|
||||||
|
};
|
||||||
|
};
|
36
ghost/members-api/util.js
Normal file
36
ghost/members-api/util.js
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getData,
|
||||||
|
handleError
|
||||||
|
};
|
Loading…
Add table
Reference in a new issue