feat(2fa): check acr values during authorization flow
This commit is contained in:
Родитель
57cccb51fa
Коммит
c20682a242
|
@ -276,4 +276,13 @@ AppError.staleAuthAt = function staleAuthAt(authAt) {
|
|||
});
|
||||
};
|
||||
|
||||
AppError.mismatchAcr = function mismatchAcr(foundValue) {
|
||||
return new AppError({
|
||||
code: 400,
|
||||
error: 'Bad Request',
|
||||
errno: 120,
|
||||
message: 'Mismatch acr value'
|
||||
}, {foundValue});
|
||||
};
|
||||
|
||||
module.exports = AppError;
|
||||
|
|
|
@ -22,6 +22,8 @@ const TOKEN = 'token';
|
|||
const ACCESS_TYPE_ONLINE = 'online';
|
||||
const ACCESS_TYPE_OFFLINE = 'offline';
|
||||
|
||||
const ACR_VALUE_AAL2 = 'AAL2';
|
||||
|
||||
const PKCE_SHA256_CHALLENGE_METHOD = 'S256'; // This server only supports S256 PKCE, no 'plain'
|
||||
const PKCE_CODE_CHALLENGE_LENGTH = 43;
|
||||
|
||||
|
@ -189,7 +191,8 @@ module.exports = {
|
|||
is: CODE,
|
||||
then: Joi.optional(),
|
||||
otherwise: Joi.forbidden()
|
||||
})
|
||||
}),
|
||||
acr_values: Joi.string().max(256).optional()
|
||||
}
|
||||
},
|
||||
response: {
|
||||
|
@ -228,6 +231,16 @@ module.exports = {
|
|||
exitEarly = true;
|
||||
throw AppError.invalidAssertion();
|
||||
}
|
||||
|
||||
// Check to see if the acr value requested by oauth matches what is expected
|
||||
const acrValues = req.payload.acr_values;
|
||||
if (acrValues) {
|
||||
const acrTokens = acrValues.split('\s+');
|
||||
if (acrTokens.includes(ACR_VALUE_AAL2) && ! (claims['fxa-aal'] >= 2)) {
|
||||
throw AppError.mismatchAcr(claims['fxa-aal']);
|
||||
}
|
||||
}
|
||||
|
||||
// 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']) {
|
||||
|
|
36
test/api.js
36
test/api.js
|
@ -119,7 +119,8 @@ function authParams(params, options) {
|
|||
assertion: AN_ASSERTION,
|
||||
client_id: options.clientId || clientId,
|
||||
state: '1',
|
||||
scope: 'a'
|
||||
scope: 'a',
|
||||
acr_values: options.acr_values || undefined
|
||||
};
|
||||
|
||||
params = params || {};
|
||||
|
@ -1001,7 +1002,6 @@ describe('/v1', function() {
|
|||
});
|
||||
});
|
||||
|
||||
|
||||
describe('response', function() {
|
||||
describe('with a trusted client', function() {
|
||||
it('should redirect to the redirect_uri', function() {
|
||||
|
@ -1025,6 +1025,38 @@ describe('/v1', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('check acr payload', () => {
|
||||
it('should throw error if mismatch with claims', () => {
|
||||
const options = {aal: 1};
|
||||
const payload = {acr_values: 'AAL2'};
|
||||
mockAssertion().reply(200, mockVerifierResult(options));
|
||||
return Server.api.post({
|
||||
url: '/authorization',
|
||||
payload: authParams(payload)
|
||||
}).then(function (res) {
|
||||
assert.equal(res.statusCode, 400);
|
||||
assertSecurityHeaders(res);
|
||||
assert.equal(res.result.message, 'Mismatch acr value');
|
||||
assert.equal(res.result.errno, 120, 'correct errno');
|
||||
});
|
||||
});
|
||||
|
||||
it('process request when correct acr_values in claims', () => {
|
||||
const options = {aal: 2};
|
||||
const payload = {acr_values: 'AAL2'};
|
||||
mockAssertion().reply(200, mockVerifierResult(options));
|
||||
return Server.api.post({
|
||||
url: '/authorization',
|
||||
payload: authParams(payload)
|
||||
}).then(function (res) {
|
||||
assert.equal(res.statusCode, 200);
|
||||
assertSecurityHeaders(res);
|
||||
assert.ok(res.result.code, 'code set');
|
||||
assert.ok(res.result.redirect, 'redirect set');
|
||||
assert.equal(res.result.state, 1, 'correct state');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('/token', function() {
|
||||
|
|
Загрузка…
Ссылка в новой задаче