feat(authorization): Require tokenVerified=true for key-bearing scopes. (#561) r=@vladikoff

The sync tokenserver does a special check for "fxa-tokenVerified" in order to enforce the use of session verification when accessing sync:

https://github.com/mozilla-services/tokenserver/blob/master/tokenserver/views.py#L140

Let's apply the same check here before granting any scopes that come with keys. In theory the user should always have a verified assertion when requesting one of these scopes, because they will have just done a keyfetch that would have required it. But there is at least one known series of calls to our backend that can yield keys without doing a verification, so it makes sense to double-check here and avoid any loopholes.
This commit is contained in:
Ryan Kelly 2018-06-10 01:33:41 +10:00 коммит произвёл Vlad Filippov
Родитель d70fe6d887
Коммит f9ad63ed6f
4 изменённых файлов: 71 добавлений и 6 удалений

Просмотреть файл

@ -238,6 +238,28 @@ module.exports = {
exitEarly = true;
throw AppError.invalidAssertion();
}
// Any request for a key-bearing scope should be using a verified token.
// Double-check that here as a defense-in-depth measure.
if (! claims['fxa-tokenVerified']) {
return P.each(scope.values(), scope => {
// Don't bother hitting the DB if other checks have failed.
if (exitEarly) {
return;
}
// We know only URL-format scopes can have keys,
// so avoid trips to the DB for common scopes like 'profile'.
if (scope.startsWith('https://')) {
return db.getScope(scope).then(s => {
if (s.hasScopedKeys) {
exitEarly = true;
throw AppError.invalidAssertion();
}
});
}
}).then(() => {
return claims;
});
}
return claims;
}),
db.getClient(Buffer.from(req.payload.client_id, 'hex')).then(function(client) {
@ -286,6 +308,9 @@ module.exports = {
checkPKCEParams(req, client);
return client;
}).catch(err => {
exitEarly = true;
throw err;
}),
scope.values(),
req

Просмотреть файл

@ -26,7 +26,7 @@ module.exports = {
payload: {
client_id: validators.clientId,
assertion: validators.assertion.required(),
scope: Joi.string()
scope: validators.scope
}
},
response: {

Просмотреть файл

@ -30,7 +30,7 @@ exports.token = Joi.string()
exports.scope = Joi.string()
.max(256)
.regex(/^[a-zA-Z0-9 _\/.:]+$/);
.regex(/^[a-zA-Z0-9 _\/.:-]+$/);
exports.redirectUri = Joi.string()
.max(256)

Просмотреть файл

@ -52,6 +52,19 @@ const VERIFY_GOOD_BUT_STALE = JSON.stringify({
'fxa-aal': AAL
}
});
const VERIFY_GOOD_BUT_UNVERIFIED = JSON.stringify({
status: 'okay',
email: USERID + '@' + config.get('browserid.issuer'),
issuer: config.get('browserid.issuer'),
idpClaims: {
'fxa-verifiedEmail': VEMAIL,
'fxa-lastAuthAt': AUTH_AT,
'fxa-generation': 123456,
'fxa-tokenVerified': false,
'fxa-amr': AMR,
'fxa-aal': AAL
}
});
const MAX_TTL_S = config.get('expiration.accessToken') / 1000;
@ -61,6 +74,11 @@ JWT_PUB_KEY.kid = 'dev-1';
JWT_PUB_KEY.use = 'sig';
JWT_PUB_KEY.alg = 'RS';
const SCOPED_CLIENT_ID = 'aaa6b9b3a65a1871';
const NO_KEY_SCOPES_CLIENT_ID = '38a6b9b3a65a1871';
const BAD_CLIENT_ID = '0006b9b3a65a1871';
const SCOPE_CAN_SCOPE_KEY = 'https://identity.mozilla.com/apps/sample-scope-can-scope-key';
function mockAssertion() {
var parts = url.parse(config.get('browserid.verificationUrl'));
@ -501,6 +519,32 @@ describe('/v1', function() {
});
});
it('succeeds by default when fxa-tokenVerified is false', function() {
mockAssertion().reply(200, VERIFY_GOOD_BUT_UNVERIFIED);
return Server.api.post({
url: '/authorization',
payload: authParams()
}).then(function(res) {
assert.equal(res.statusCode, 200);
assertSecurityHeaders(res);
});
});
it('errors when fxa-tokenVerified is false and a scope has keys', function() {
mockAssertion().reply(200, VERIFY_GOOD_BUT_UNVERIFIED);
return Server.api.post({
url: '/authorization',
payload: authParams({
client_id: SCOPED_CLIENT_ID,
scope: SCOPE_CAN_SCOPE_KEY
})
}).then(function(res) {
assert.equal(res.result.code, 401);
assert.equal(res.result.message, 'Invalid assertion');
assertSecurityHeaders(res);
});
});
});
describe('?redirect_uri', function() {
@ -2542,10 +2586,6 @@ describe('/v1', function() {
});
describe('POST /key-data', function() {
const SCOPED_CLIENT_ID = 'aaa6b9b3a65a1871';
const NO_KEY_SCOPES_CLIENT_ID = '38a6b9b3a65a1871';
const BAD_CLIENT_ID = '0006b9b3a65a1871';
const SCOPE_CAN_SCOPE_KEY = 'https://identity.mozilla.com/apps/sample-scope-can-scope-key';
let genericRequest;
beforeEach(function () {