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;
|
module.exports = AppError;
|
||||||
|
|
|
@ -22,6 +22,8 @@ const TOKEN = 'token';
|
||||||
const ACCESS_TYPE_ONLINE = 'online';
|
const ACCESS_TYPE_ONLINE = 'online';
|
||||||
const ACCESS_TYPE_OFFLINE = 'offline';
|
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_SHA256_CHALLENGE_METHOD = 'S256'; // This server only supports S256 PKCE, no 'plain'
|
||||||
const PKCE_CODE_CHALLENGE_LENGTH = 43;
|
const PKCE_CODE_CHALLENGE_LENGTH = 43;
|
||||||
|
|
||||||
|
@ -189,7 +191,8 @@ module.exports = {
|
||||||
is: CODE,
|
is: CODE,
|
||||||
then: Joi.optional(),
|
then: Joi.optional(),
|
||||||
otherwise: Joi.forbidden()
|
otherwise: Joi.forbidden()
|
||||||
})
|
}),
|
||||||
|
acr_values: Joi.string().max(256).optional()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
response: {
|
response: {
|
||||||
|
@ -228,6 +231,16 @@ module.exports = {
|
||||||
exitEarly = true;
|
exitEarly = true;
|
||||||
throw AppError.invalidAssertion();
|
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.
|
// Any request for a key-bearing scope should be using a verified token.
|
||||||
// Double-check that here as a defense-in-depth measure.
|
// Double-check that here as a defense-in-depth measure.
|
||||||
if (! claims['fxa-tokenVerified']) {
|
if (! claims['fxa-tokenVerified']) {
|
||||||
|
|
36
test/api.js
36
test/api.js
|
@ -119,7 +119,8 @@ function authParams(params, options) {
|
||||||
assertion: AN_ASSERTION,
|
assertion: AN_ASSERTION,
|
||||||
client_id: options.clientId || clientId,
|
client_id: options.clientId || clientId,
|
||||||
state: '1',
|
state: '1',
|
||||||
scope: 'a'
|
scope: 'a',
|
||||||
|
acr_values: options.acr_values || undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
params = params || {};
|
params = params || {};
|
||||||
|
@ -1001,7 +1002,6 @@ describe('/v1', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
describe('response', function() {
|
describe('response', function() {
|
||||||
describe('with a trusted client', function() {
|
describe('with a trusted client', function() {
|
||||||
it('should redirect to the redirect_uri', 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() {
|
describe('/token', function() {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче