feat(keys): Check lastAuthAt freshness when fetching key data. (#506) r=@vladikoff
This commit is contained in:
Родитель
4f613d33d4
Коммит
e0de2f3b57
|
@ -127,6 +127,12 @@ const conf = convict({
|
||||||
format: 'duration',
|
format: 'duration',
|
||||||
default: '15 minutes',
|
default: '15 minutes',
|
||||||
env: 'FXA_EXPIRATION_CODE'
|
env: 'FXA_EXPIRATION_CODE'
|
||||||
|
},
|
||||||
|
keyDataAuth: {
|
||||||
|
doc: 'Key data can only be fetched if lastAuthAt is within this duration',
|
||||||
|
format: 'duration',
|
||||||
|
default: '1 hour',
|
||||||
|
env: 'FXA_EXPIRATION_KEY_DATA_AUTH'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refreshToken: {
|
refreshToken: {
|
||||||
|
|
12
lib/error.js
12
lib/error.js
|
@ -116,6 +116,7 @@ AppError.invalidAssertion = function invalidAssertion() {
|
||||||
message: 'Invalid assertion'
|
message: 'Invalid assertion'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
AppError.unknownCode = function unknownCode(code) {
|
AppError.unknownCode = function unknownCode(code) {
|
||||||
return new AppError({
|
return new AppError({
|
||||||
code: 400,
|
code: 400,
|
||||||
|
@ -264,4 +265,15 @@ AppError.missingPkceParameters = function missingPkceParameters() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AppError.staleAuthAt = function staleAuthAt(authAt) {
|
||||||
|
return new AppError({
|
||||||
|
code: 401,
|
||||||
|
error: 'Bad Request',
|
||||||
|
errno: 119,
|
||||||
|
message: 'Stale authentication timestamp'
|
||||||
|
}, {
|
||||||
|
authAt: authAt
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = AppError;
|
module.exports = AppError;
|
||||||
|
|
|
@ -11,6 +11,9 @@ const P = require('../promise');
|
||||||
const validators = require('../validators');
|
const validators = require('../validators');
|
||||||
const verify = require('../browserid');
|
const verify = require('../browserid');
|
||||||
const Scope = require('../scope');
|
const Scope = require('../scope');
|
||||||
|
const config = require('../config');
|
||||||
|
|
||||||
|
const AUTH_EXPIRES_AFTER_MS = config.get('expiration.keyDataAuth');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* We're using a static value for key material on purpose, in future this value can read from the DB.
|
* We're using a static value for key material on purpose, in future this value can read from the DB.
|
||||||
|
@ -71,6 +74,10 @@ module.exports = {
|
||||||
const scopeResults = results[1];
|
const scopeResults = results[1];
|
||||||
const response = {};
|
const response = {};
|
||||||
|
|
||||||
|
if (assertionData['fxa-lastAuthAt'] < (Date.now() - AUTH_EXPIRES_AFTER_MS) / 1000) {
|
||||||
|
throw AppError.staleAuthAt(assertionData['fxa-lastAuthAt']);
|
||||||
|
}
|
||||||
|
|
||||||
scopeResults.forEach((keyScope) => {
|
scopeResults.forEach((keyScope) => {
|
||||||
response[keyScope.scope] = {
|
response[keyScope.scope] = {
|
||||||
identifier: keyScope.scope,
|
identifier: keyScope.scope,
|
||||||
|
|
32
test/api.js
32
test/api.js
|
@ -21,13 +21,25 @@ const assertSecurityHeaders = require('./lib/util').assertSecurityHeaders;
|
||||||
|
|
||||||
const USERID = unique(16).toString('hex');
|
const USERID = unique(16).toString('hex');
|
||||||
const VEMAIL = unique(4).toString('hex') + '@mozilla.com';
|
const VEMAIL = unique(4).toString('hex') + '@mozilla.com';
|
||||||
|
const AUTH_AT = Math.floor(Date.now() / 1000);
|
||||||
|
const STALE_AUTH_AT = AUTH_AT - (2 * 24 * 60 * 60);
|
||||||
const VERIFY_GOOD = JSON.stringify({
|
const VERIFY_GOOD = JSON.stringify({
|
||||||
status: 'okay',
|
status: 'okay',
|
||||||
email: USERID + '@' + config.get('browserid.issuer'),
|
email: USERID + '@' + config.get('browserid.issuer'),
|
||||||
issuer: config.get('browserid.issuer'),
|
issuer: config.get('browserid.issuer'),
|
||||||
idpClaims: {
|
idpClaims: {
|
||||||
'fxa-verifiedEmail': VEMAIL,
|
'fxa-verifiedEmail': VEMAIL,
|
||||||
'fxa-lastAuthAt': 123456,
|
'fxa-lastAuthAt': AUTH_AT,
|
||||||
|
'fxa-generation': 123456
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const VERIFY_GOOD_BUT_STALE = JSON.stringify({
|
||||||
|
status: 'okay',
|
||||||
|
email: USERID + '@' + config.get('browserid.issuer'),
|
||||||
|
issuer: config.get('browserid.issuer'),
|
||||||
|
idpClaims: {
|
||||||
|
'fxa-verifiedEmail': VEMAIL,
|
||||||
|
'fxa-lastAuthAt': STALE_AUTH_AT,
|
||||||
'fxa-generation': 123456
|
'fxa-generation': 123456
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1226,7 +1238,7 @@ describe('/v1', function() {
|
||||||
assert.equal(res.result.access_token.length,
|
assert.equal(res.result.access_token.length,
|
||||||
config.get('unique.token') * 2);
|
config.get('unique.token') * 2);
|
||||||
assert.equal(res.result.scope, 'foo bar');
|
assert.equal(res.result.scope, 'foo bar');
|
||||||
assert.equal(res.result.auth_at, 123456);
|
assert.equal(res.result.auth_at, AUTH_AT);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -1262,7 +1274,7 @@ describe('/v1', function() {
|
||||||
assert.equal(res.result.refresh_token.length,
|
assert.equal(res.result.refresh_token.length,
|
||||||
config.get('unique.token') * 2);
|
config.get('unique.token') * 2);
|
||||||
assert.equal(res.result.scope, 'foo bar');
|
assert.equal(res.result.scope, 'foo bar');
|
||||||
assert.equal(res.result.auth_at, 123456);
|
assert.equal(res.result.auth_at, AUTH_AT);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -2336,6 +2348,20 @@ describe('/v1', function() {
|
||||||
assert.equal(Object.keys(res.result).length, 0, 'no scoped keys');
|
assert.equal(Object.keys(res.result).length, 0, 'no scoped keys');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('fails for assertions with lastAuthAt too far in the past', () => {
|
||||||
|
genericRequest.payload.client_id = NO_KEY_SCOPES_CLIENT_ID;
|
||||||
|
|
||||||
|
mockAssertion().reply(200, VERIFY_GOOD_BUT_STALE);
|
||||||
|
return Server.api.post(genericRequest)
|
||||||
|
.then((res) => {
|
||||||
|
assert.equal(res.statusCode, 401);
|
||||||
|
assertSecurityHeaders(res);
|
||||||
|
const body = res.result;
|
||||||
|
assert.equal(body.errno, 119);
|
||||||
|
assert.equal(body.message, 'Stale authentication timestamp');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
Загрузка…
Ссылка в новой задаче