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:
Родитель
d70fe6d887
Коммит
f9ad63ed6f
|
@ -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)
|
||||
|
|
48
test/api.js
48
test/api.js
|
@ -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 () {
|
||||
|
|
Загрузка…
Ссылка в новой задаче