implemented PKCE auth (#921)

* implemented PKCE auth

* removed node-jose

* added PKCE tests
This commit is contained in:
Danny Coates 2018-09-14 08:00:33 -07:00 committed by Donovan Preston
parent 20528eb0d1
commit 7ccf462bf8
18 changed files with 331 additions and 263 deletions

View file

@ -134,11 +134,6 @@ const conf = convict({
format: String,
default: 'b50ec33d3c9beb6d', // localhost
env: 'FXA_CLIENT_ID'
},
fxa_client_secret: {
format: String,
default: '05ac76fbe3e739c9effbaea439bc07d265c613c5e0da9070590a2378377c09d8', // localhost
env: 'FXA_CLIENT_SECRET'
}
});

46
server/fxa.js Normal file
View file

@ -0,0 +1,46 @@
const fetch = require('node-fetch');
const config = require('./config');
const KEY_SCOPE = 'https://identity.mozilla.com/apps/send';
let fxaConfig = null;
let lastConfigRefresh = 0;
async function getFxaConfig() {
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
return fxaConfig;
}
const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
fxaConfig = await res.json();
lastConfigRefresh = Date.now();
return fxaConfig;
}
module.exports = {
getFxaConfig,
verify: async function(token) {
if (!token) {
return null;
}
const c = await getFxaConfig();
try {
const verifyUrl = c.jwks_uri.replace('jwks', 'verify'); //HACK
const result = await fetch(verifyUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const info = await result.json();
if (
info.scope &&
Array.isArray(info.scope) &&
info.scope.includes(KEY_SCOPE)
) {
return info.user;
}
} catch (e) {
// gulp
}
return null;
}
};

View file

@ -6,9 +6,6 @@ module.exports = function(state) {
return state.cspNonce
? html`
<script nonce="${state.cspNonce}">
const userInfo = ${
state.user.loggedIn ? raw(JSON.stringify(state.user)) : 'null'
};
const downloadMetadata = ${
state.downloadMetadata ? raw(JSON.stringify(state.downloadMetadata)) : '{}'
};

View file

@ -1,6 +1,6 @@
const crypto = require('crypto');
const storage = require('../storage');
const fxa = require('../routes/fxa');
const fxa = require('../fxa');
module.exports = {
hmac: async function(req, res, next) {

View file

@ -1,96 +0,0 @@
const { URLSearchParams } = require('url');
const fetch = require('node-fetch');
const config = require('../config');
const pages = require('./pages');
const KEY_SCOPE = 'https://identity.mozilla.com/apps/send';
let fxaConfig = null;
let lastConfigRefresh = 0;
async function getFxaConfig() {
if (fxaConfig && Date.now() - lastConfigRefresh < 1000 * 60 * 5) {
return fxaConfig;
}
const res = await fetch(`${config.fxa_url}/.well-known/openid-configuration`);
fxaConfig = await res.json();
lastConfigRefresh = Date.now();
return fxaConfig;
}
module.exports = {
login: async function(req, res) {
const query = req.query;
if (!query || !query.keys_jwk) {
return res.sendStatus(400);
}
const c = await getFxaConfig();
const params = new URLSearchParams({
client_id: config.fxa_client_id,
redirect_uri: `${config.base_url}/api/fxa/oauth`,
state: 'todo',
scope: `profile ${KEY_SCOPE}`,
action: 'email',
keys_jwk: query.keys_jwk
});
res.redirect(`${c.authorization_endpoint}?${params.toString()}`);
},
oauth: async function(req, res) {
const query = req.query;
if (!query || !query.code || !query.state || !query.action) {
return res.sendStatus(400);
}
const c = await getFxaConfig();
const x = await fetch(c.token_endpoint, {
method: 'POST',
body: JSON.stringify({
code: query.code,
client_id: config.fxa_client_id,
client_secret: config.fxa_client_secret
}),
headers: {
'content-type': 'application/json'
}
});
const zzz = await x.json();
console.error(zzz);
const p = await fetch(c.userinfo_endpoint, {
method: 'GET',
headers: {
authorization: `Bearer ${zzz.access_token}`
}
});
const userInfo = await p.json();
userInfo.keys_jwe = zzz.keys_jwe;
userInfo.access_token = zzz.access_token;
req.userInfo = userInfo;
pages.index(req, res);
},
verify: async function(token) {
if (!token) {
return null;
}
const c = await getFxaConfig();
try {
const verifyUrl = c.jwks_uri.replace('jwks', 'verify');
const result = await fetch(verifyUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token })
});
const info = await result.json();
if (
info.scope &&
Array.isArray(info.scope) &&
info.scope.includes(KEY_SCOPE)
) {
return info.user;
}
} catch (e) {
// gulp
}
return null;
}
};

View file

@ -6,7 +6,6 @@ const config = require('../config');
const auth = require('../middleware/auth');
const language = require('../middleware/language');
const pages = require('./pages');
const fxa = require('./fxa');
const filelist = require('./filelist');
const IS_DEV = config.env === 'development';
@ -34,6 +33,8 @@ module.exports = function(app) {
'wss://*.dev.lcip.org',
'wss://*.mozaws.net',
'wss://send.firefox.com',
'https://*.dev.lcip.org',
'https://*.accounts.firefox.com',
'https://sentry.prod.mozaws.net',
'https://www.google-analytics.com'
],
@ -80,8 +81,7 @@ module.exports = function(app) {
);
app.get(`/api/exists/:id${ID_REGEX}`, require('./exists'));
app.get(`/api/metadata/:id${ID_REGEX}`, auth.hmac, require('./metadata'));
app.get('/api/fxa/login', fxa.login);
app.get('/api/fxa/oauth', fxa.oauth);
app.get('/api/fxa/oauth', pages.blank);
app.get('/api/filelist', auth.fxa, filelist.get);
app.post('/api/filelist', auth.fxa, filelist.post);
app.post('/api/upload', auth.fxa, require('./upload'));

View file

@ -1,4 +1,5 @@
const config = require('../config');
const { getFxaConfig } = require('../fxa');
let sentry = '';
if (config.sentry_id) {
@ -27,33 +28,35 @@ if (config.analytics_id) {
ga = `var GOOGLE_ANALYTICS_ID = '${config.analytics_id}';`;
}
/* eslint-disable no-useless-escape */
const jsconfig = `
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
if (isIE && !isUnsupportedPage) {
window.location.replace('/unsupported/ie');
}
var LIMITS = {
ANON: {
MAX_FILE_SIZE: ${config.anon_max_file_size},
MAX_DOWNLOADS: ${config.anon_max_downloads},
MAX_EXPIRE_SECONDS: ${config.anon_max_expire_seconds},
},
MAX_FILE_SIZE: ${config.max_file_size},
MAX_DOWNLOADS: ${config.max_downloads},
MAX_EXPIRE_SECONDS: ${config.max_expire_seconds},
MAX_FILES_PER_ARCHIVE: ${config.max_files_per_archive},
MAX_ARCHIVES_PER_USER: ${config.max_archives_per_user}
};
var DEFAULTS = {
EXPIRE_SECONDS: ${config.default_expire_seconds}
};
${ga}
${sentry}
`;
module.exports = function(req, res) {
module.exports = async function(req, res) {
const fxaConfig = await getFxaConfig();
fxaConfig.client_id = config.fxa_client_id;
/* eslint-disable no-useless-escape */
const jsconfig = `
var isIE = /trident\\\/7\.|msie/i.test(navigator.userAgent);
var isUnsupportedPage = /\\\/unsupported/.test(location.pathname);
if (isIE && !isUnsupportedPage) {
window.location.replace('/unsupported/ie');
}
var LIMITS = {
ANON: {
MAX_FILE_SIZE: ${config.anon_max_file_size},
MAX_DOWNLOADS: ${config.anon_max_downloads},
MAX_EXPIRE_SECONDS: ${config.anon_max_expire_seconds},
},
MAX_FILE_SIZE: ${config.max_file_size},
MAX_DOWNLOADS: ${config.max_downloads},
MAX_EXPIRE_SECONDS: ${config.max_expire_seconds},
MAX_FILES_PER_ARCHIVE: ${config.max_files_per_archive},
MAX_ARCHIVES_PER_USER: ${config.max_archives_per_user}
};
var DEFAULTS = {
EXPIRE_SECONDS: ${config.default_expire_seconds}
};
var AUTH_CONFIG = ${JSON.stringify(fxaConfig)};
${ga}
${sentry}
`;
res.set('Content-Type', 'application/javascript');
res.send(jsconfig);
};

View file

@ -5,7 +5,7 @@ const mozlog = require('../log');
const Limiter = require('../limiter');
const Parser = require('../streamparser');
const wsStream = require('websocket-stream/stream');
const fxa = require('./fxa');
const fxa = require('../fxa');
const log = mozlog('send.upload');

View file

@ -5,8 +5,6 @@ const assets = require('../common/assets');
module.exports = function(req) {
const locale = req.language || 'en-US';
const userInfo = req.userInfo || { avatar: assets.get('user.svg') };
userInfo.loggedIn = !!userInfo.access_token;
return {
locale,
translate: locales.getTranslator(locale),
@ -21,7 +19,7 @@ module.exports = function(req) {
fira: false,
fileInfo: {},
cspNonce: req.cspNonce,
user: userInfo,
user: { avatar: assets.get('user.svg'), loggedIn: false },
layout
};
};