mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-01-01 23:37:43 +03:00
Improved Members security and performance (#10511)
no-issue * Corrected function names for rpc methods * Updated gateway to store tokens locally * Fixed lint * Added hardcoded 30 minute expiry for member tokens * Added default contentApiAccess config; * Updated validateAudience method This is required for security, we need to restrict which domains can access tokens meant for the content api
This commit is contained in:
parent
93752b7b1b
commit
a02a43e6fa
@ -20,6 +20,7 @@
|
||||
"active": "SchedulingDefault"
|
||||
},
|
||||
"members": {
|
||||
"contentApiAccess": [],
|
||||
"paymentProcessors": []
|
||||
},
|
||||
"logging": {
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* global window document location fetch */
|
||||
/* global atob window document location fetch */
|
||||
(function () {
|
||||
if (window.parent === window) {
|
||||
return;
|
||||
@ -23,9 +23,76 @@
|
||||
};
|
||||
}
|
||||
|
||||
function isTokenExpired(token) {
|
||||
try {
|
||||
const [header, claims, signature] = token.split('.'); // eslint-disable-line no-unused-vars
|
||||
|
||||
const parsedClaims = JSON.parse(atob(claims.replace('+', '-').replace('/', '_')));
|
||||
|
||||
const expiry = parsedClaims.exp * 1000;
|
||||
const now = Date.now();
|
||||
|
||||
const nearFuture = now + (30 * 1000);
|
||||
|
||||
if (expiry > nearFuture) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function getStoredToken(audience) {
|
||||
const tokenKey = 'members:token:aud:' + audience;
|
||||
const storedToken = storage.getItem(tokenKey);
|
||||
if (isTokenExpired(storedToken)) {
|
||||
storage.removeItem(tokenKey);
|
||||
return null;
|
||||
}
|
||||
return storedToken;
|
||||
}
|
||||
|
||||
function getStoredTokenKeys() {
|
||||
try {
|
||||
return JSON.parse(storage.getItem('members:tokens') || '[]');
|
||||
} catch (e) {
|
||||
storage.removeItem('members:tokens');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function addStoredToken(audience, token) {
|
||||
const storedTokenKeys = getStoredTokenKeys();
|
||||
const tokenKey = 'members:token:aud:' + audience;
|
||||
|
||||
storage.setItem(tokenKey, token);
|
||||
if (!storedTokenKeys.includes(tokenKey)) {
|
||||
storage.setItem('members:tokens', JSON.stringify(storedTokenKeys.concat(tokenKey)));
|
||||
}
|
||||
}
|
||||
|
||||
function clearStorage() {
|
||||
storage.removeItem('signedin');
|
||||
const storedTokenKeys = getStoredTokenKeys();
|
||||
|
||||
storedTokenKeys.forEach(function (key) {
|
||||
storage.removeItem(key);
|
||||
});
|
||||
|
||||
storage.removeItem('members:tokens');
|
||||
}
|
||||
|
||||
// @TODO this needs to be configurable
|
||||
const membersApi = location.pathname.replace(/\/members\/gateway\/?$/, '/ghost/api/v2/members');
|
||||
function getToken({audience}) {
|
||||
const storedToken = getStoredToken(audience);
|
||||
|
||||
if (storedToken) {
|
||||
return Promise.resolve(storedToken);
|
||||
}
|
||||
|
||||
return fetch(`${membersApi}/token`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -44,6 +111,11 @@
|
||||
}
|
||||
storage.setItem('signedin', true);
|
||||
return res.text();
|
||||
}).then(function (token) {
|
||||
if (token) {
|
||||
addStoredToken(audience, token);
|
||||
}
|
||||
return token;
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,13 +202,13 @@
|
||||
})
|
||||
}).then((res) => {
|
||||
if (res.ok) {
|
||||
storage.removeItem('signedin');
|
||||
clearStorage();
|
||||
}
|
||||
return res.ok;
|
||||
});
|
||||
});
|
||||
|
||||
addMethod('requestPasswordReset', function signout({email}) {
|
||||
addMethod('requestPasswordReset', function requestPasswordReset({email}) {
|
||||
return fetch(`${membersApi}/request-password-reset`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -151,7 +223,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
addMethod('resetPassword', function signout({token, password}) {
|
||||
addMethod('resetPassword', function resetPassword({token, password}) {
|
||||
return fetch(`${membersApi}/reset-password`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
@ -17,6 +17,7 @@ module.exports = function ({
|
||||
}, privateKey, {
|
||||
algorithm: 'RS512',
|
||||
audience: aud,
|
||||
expiresIn: '30m',
|
||||
issuer
|
||||
}));
|
||||
}
|
||||
|
@ -59,14 +59,6 @@ function validateMember({email, password}) {
|
||||
});
|
||||
}
|
||||
|
||||
// @TODO this should check some config/settings and return Promise.reject by default
|
||||
function validateAudience({audience, origin}) {
|
||||
if (audience === origin) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const publicKey = settingsCache.get('members_public_key');
|
||||
const privateKey = settingsCache.get('members_private_key');
|
||||
const sessionSecret = settingsCache.get('members_session_secret');
|
||||
@ -79,6 +71,18 @@ let mailer;
|
||||
|
||||
const membersConfig = config.get('members');
|
||||
|
||||
function validateAudience({audience, origin}) {
|
||||
if (audience === origin) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (audience === siteOrigin) {
|
||||
if (membersConfig.contentApiAccess.includes(origin)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
function sendEmail(member, {token}) {
|
||||
if (!(mailer instanceof mail.GhostMailer)) {
|
||||
mailer = new mail.GhostMailer();
|
||||
|
Loading…
Reference in New Issue
Block a user