152 lines
6.3 KiB
JavaScript
152 lines
6.3 KiB
JavaScript
import { Response } from 'miragejs';
|
|
import Ember from 'ember';
|
|
import fetch from 'fetch';
|
|
|
|
export default function (server) {
|
|
// initial auth response cache -- lookup by mfa_request_id key
|
|
const authResponses = {};
|
|
// mfa requirement cache -- lookup by mfa_request_id key
|
|
const mfaRequirement = {};
|
|
// generate different constraint scenarios and return mfa_requirement object
|
|
const generateMfaRequirement = (req, res) => {
|
|
const { user } = req.params;
|
|
// uses_passcode automatically set to true in factory for totp type
|
|
const m = (type, uses_passcode = false) => server.create('mfa-method', { type, uses_passcode });
|
|
let mfa_constraints = {};
|
|
let methods = []; // flat array of methods for easy lookup during validation
|
|
|
|
function generator() {
|
|
const methods = [];
|
|
const constraintObj = [...arguments].reduce((obj, methodArray, index) => {
|
|
obj[`test_${index}`] = { any: methodArray };
|
|
methods.push(...methodArray);
|
|
return obj;
|
|
}, {});
|
|
return [constraintObj, methods];
|
|
}
|
|
|
|
if (user === 'mfa-a') {
|
|
[mfa_constraints, methods] = generator([m('totp')]); // 1 constraint 1 passcode
|
|
} else if (user === 'mfa-b') {
|
|
[mfa_constraints, methods] = generator([m('okta')]); // 1 constraint 1 non-passcode
|
|
} else if (user === 'mfa-c') {
|
|
[mfa_constraints, methods] = generator([m('totp'), m('duo', true)]); // 1 constraint 2 passcodes
|
|
} else if (user === 'mfa-d') {
|
|
[mfa_constraints, methods] = generator([m('okta'), m('duo')]); // 1 constraint 2 non-passcode
|
|
} else if (user === 'mfa-e') {
|
|
[mfa_constraints, methods] = generator([m('okta'), m('totp')]); // 1 constraint 1 passcode 1 non-passcode
|
|
} else if (user === 'mfa-f') {
|
|
[mfa_constraints, methods] = generator([m('totp')], [m('duo', true)]); // 2 constraints 1 passcode for each
|
|
} else if (user === 'mfa-g') {
|
|
[mfa_constraints, methods] = generator([m('okta')], [m('duo')]); // 2 constraints 1 non-passcode for each
|
|
} else if (user === 'mfa-h') {
|
|
[mfa_constraints, methods] = generator([m('totp')], [m('okta')]); // 2 constraints 1 passcode 1 non-passcode
|
|
} else if (user === 'mfa-i') {
|
|
[mfa_constraints, methods] = generator([m('okta'), m('totp')], [m('totp')]); // 2 constraints 1 passcode/1 non-passcode 1 non-passcode
|
|
} else if (user === 'mfa-j') {
|
|
[mfa_constraints, methods] = generator([m('pingid')]); // use to test push failures
|
|
}
|
|
const numbers = (length) =>
|
|
Math.random()
|
|
.toString()
|
|
.substring(2, length + 2);
|
|
const mfa_request_id = `${numbers(8)}-${numbers(4)}-${numbers(4)}-${numbers(4)}-${numbers(12)}`;
|
|
const mfa_requirement = {
|
|
mfa_request_id,
|
|
mfa_constraints,
|
|
};
|
|
// cache mfa requests to test different validation scenarios
|
|
mfaRequirement[mfa_request_id] = { methods };
|
|
// cache auth response to be returned later by sys/mfa/validate
|
|
authResponses[mfa_request_id] = { ...res };
|
|
return mfa_requirement;
|
|
};
|
|
// passthrough original request, cache response and return mfa stub
|
|
const passthroughLogin = async (schema, req) => {
|
|
// test totp not configured scenario
|
|
if (req.params.user === 'totp-na') {
|
|
return new Response(400, {}, { errors: ['TOTP mfa required but not configured'] });
|
|
}
|
|
const mock = req.params.user ? req.params.user.includes('mfa') : null;
|
|
// bypass mfa for users that do not match type
|
|
if (!mock) {
|
|
req.passthrough();
|
|
} else if (Ember.testing) {
|
|
// use root token in test environment
|
|
const res = await fetch('/v1/auth/token/lookup-self', { headers: { 'X-Vault-Token': 'root' } });
|
|
if (res.status < 300) {
|
|
const json = res.json();
|
|
if (Ember.testing) {
|
|
json.auth = {
|
|
...json.data,
|
|
policies: [],
|
|
metadata: { username: 'foobar' },
|
|
};
|
|
json.data = null;
|
|
}
|
|
return { auth: { mfa_requirement: generateMfaRequirement(req, json) } };
|
|
}
|
|
return new Response(500, {}, { errors: ['Mirage error fetching root token in testing'] });
|
|
} else {
|
|
const xhr = req.passthrough();
|
|
xhr.onreadystatechange = () => {
|
|
if (xhr.readyState === 4 && xhr.status < 300) {
|
|
// XMLHttpRequest response prop only has a getter -- redefine as writable and set value
|
|
Object.defineProperty(xhr, 'response', {
|
|
writable: true,
|
|
value: JSON.stringify({
|
|
auth: { mfa_requirement: generateMfaRequirement(req, JSON.parse(xhr.responseText)) },
|
|
}),
|
|
});
|
|
}
|
|
};
|
|
}
|
|
};
|
|
server.post('/auth/:method/login/:user', passthroughLogin);
|
|
|
|
server.post('/sys/mfa/validate', (schema, req) => {
|
|
try {
|
|
const { mfa_request_id, mfa_payload } = JSON.parse(req.requestBody);
|
|
const mfaRequest = mfaRequirement[mfa_request_id];
|
|
|
|
if (!mfaRequest) {
|
|
return new Response(404, {}, { errors: ['MFA Request ID not found'] });
|
|
}
|
|
// validate request body
|
|
for (let constraintId in mfa_payload) {
|
|
// ensure ids were passed in map
|
|
const method = mfaRequest.methods.find(({ id }) => id === constraintId);
|
|
if (!method) {
|
|
return new Response(
|
|
400,
|
|
{},
|
|
{ errors: [`Invalid MFA constraint id ${constraintId} passed in map`] }
|
|
);
|
|
}
|
|
// test non-totp validation by rejecting all pingid requests
|
|
if (method.type === 'pingid') {
|
|
return new Response(403, {}, { errors: ['PingId MFA validation failed'] });
|
|
}
|
|
// validate totp passcode
|
|
const passcode = mfa_payload[constraintId][0];
|
|
if (method.uses_passcode) {
|
|
if (passcode !== 'test') {
|
|
const error =
|
|
passcode === 'used'
|
|
? 'code already used; new code is available in 30 seconds'
|
|
: 'failed to validate';
|
|
return new Response(403, {}, { errors: [error] });
|
|
}
|
|
} else if (passcode) {
|
|
// for okta and duo, reject if a passcode was provided
|
|
return new Response(400, {}, { errors: ['Passcode should only be provided for TOTP MFA type'] });
|
|
}
|
|
}
|
|
return authResponses[mfa_request_id];
|
|
} catch (error) {
|
|
console.log(error);
|
|
return new Response(500, {}, { errors: ['Mirage Handler Error: /sys/mfa/validate'] });
|
|
}
|
|
});
|
|
}
|