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:
Fabien O'Carroll 2019-02-23 04:47:42 +01:00 committed by GitHub
parent 93752b7b1b
commit a02a43e6fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 90 additions and 12 deletions

View File

@ -20,6 +20,7 @@
"active": "SchedulingDefault"
},
"members": {
"contentApiAccess": [],
"paymentProcessors": []
},
"logging": {

View File

@ -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: {

View File

@ -17,6 +17,7 @@ module.exports = function ({
}, privateKey, {
algorithm: 'RS512',
audience: aud,
expiresIn: '30m',
issuer
}));
}

View File

@ -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();